1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app;
18 
19 import android.animation.LayoutTransition;
20 import android.app.FragmentManager.BackStackEntry;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.util.AttributeSet;
24 import android.view.Gravity;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.LinearLayout;
29 import android.widget.TextView;
30 
31 /**
32  * Helper class for showing "bread crumbs" representing the fragment
33  * stack in an activity.  This is intended to be used with
34  * {@link ActionBar#setCustomView(View)
35  * ActionBar.setCustomView(View)} to place the bread crumbs in
36  * the action bar.
37  *
38  * <p>The default style for this view is
39  * {@link android.R.style#Widget_FragmentBreadCrumbs}.
40  *
41  * @deprecated This widget is no longer supported.
42  */
43 @Deprecated
44 public class FragmentBreadCrumbs extends ViewGroup
45         implements FragmentManager.OnBackStackChangedListener {
46     Activity mActivity;
47     LayoutInflater mInflater;
48     LinearLayout mContainer;
49     int mMaxVisible = -1;
50 
51     // Hahah
52     BackStackRecord mTopEntry;
53     BackStackRecord mParentEntry;
54 
55     /** Listener to inform when a parent entry is clicked */
56     private OnClickListener mParentClickListener;
57 
58     private OnBreadCrumbClickListener mOnBreadCrumbClickListener;
59 
60     private int mGravity;
61     private int mLayoutResId;
62     private int mTextColor;
63 
64     private static final int DEFAULT_GRAVITY = Gravity.START | Gravity.CENTER_VERTICAL;
65 
66     /**
67      * Interface to intercept clicks on the bread crumbs.
68      *
69      * @deprecated This widget is no longer supported.
70      */
71     @Deprecated
72     public interface OnBreadCrumbClickListener {
73         /**
74          * Called when a bread crumb is clicked.
75          *
76          * @param backStack The BackStackEntry whose bread crumb was clicked.
77          * May be null, if this bread crumb is for the root of the back stack.
78          * @param flags Additional information about the entry.  Currently
79          * always 0.
80          *
81          * @return Return true to consume this click.  Return to false to allow
82          * the default action (popping back stack to this entry) to occur.
83          */
onBreadCrumbClick(BackStackEntry backStack, int flags)84         public boolean onBreadCrumbClick(BackStackEntry backStack, int flags);
85     }
86 
FragmentBreadCrumbs(Context context)87     public FragmentBreadCrumbs(Context context) {
88         this(context, null);
89     }
90 
FragmentBreadCrumbs(Context context, AttributeSet attrs)91     public FragmentBreadCrumbs(Context context, AttributeSet attrs) {
92         this(context, attrs, com.android.internal.R.attr.fragmentBreadCrumbsStyle);
93     }
94 
FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr)95     public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr) {
96         this(context, attrs, defStyleAttr, 0);
97     }
98 
99     /**
100      * @hide
101      */
FragmentBreadCrumbs( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)102     public FragmentBreadCrumbs(
103             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
104         super(context, attrs, defStyleAttr, defStyleRes);
105 
106         final TypedArray a = context.obtainStyledAttributes(attrs,
107                 com.android.internal.R.styleable.FragmentBreadCrumbs, defStyleAttr, defStyleRes);
108 
109         mGravity = a.getInt(com.android.internal.R.styleable.FragmentBreadCrumbs_gravity,
110                 DEFAULT_GRAVITY);
111         mLayoutResId = a.getResourceId(
112                 com.android.internal.R.styleable.FragmentBreadCrumbs_itemLayout,
113                 com.android.internal.R.layout.fragment_bread_crumb_item);
114         mTextColor = a.getColor(
115                 com.android.internal.R.styleable.FragmentBreadCrumbs_itemColor,
116                 0);
117 
118         a.recycle();
119     }
120 
121     /**
122      * Attach the bread crumbs to their activity.  This must be called once
123      * when creating the bread crumbs.
124      */
setActivity(Activity a)125     public void setActivity(Activity a) {
126         mActivity = a;
127         mInflater = (LayoutInflater)a.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
128         mContainer = (LinearLayout)mInflater.inflate(
129                 com.android.internal.R.layout.fragment_bread_crumbs,
130                 this, false);
131         addView(mContainer);
132         a.getFragmentManager().addOnBackStackChangedListener(this);
133         updateCrumbs();
134         setLayoutTransition(new LayoutTransition());
135     }
136 
137     /**
138      * The maximum number of breadcrumbs to show. Older fragment headers will be hidden from view.
139      * @param visibleCrumbs the number of visible breadcrumbs. This should be greater than zero.
140      */
setMaxVisible(int visibleCrumbs)141     public void setMaxVisible(int visibleCrumbs) {
142         if (visibleCrumbs < 1) {
143             throw new IllegalArgumentException("visibleCrumbs must be greater than zero");
144         }
145         mMaxVisible = visibleCrumbs;
146     }
147 
148     /**
149      * Inserts an optional parent entry at the first position in the breadcrumbs. Selecting this
150      * entry will result in a call to the specified listener's
151      * {@link android.view.View.OnClickListener#onClick(View)}
152      * method.
153      *
154      * @param title the title for the parent entry
155      * @param shortTitle the short title for the parent entry
156      * @param listener the {@link android.view.View.OnClickListener} to be called when clicked.
157      * A null will result in no action being taken when the parent entry is clicked.
158      */
setParentTitle(CharSequence title, CharSequence shortTitle, OnClickListener listener)159     public void setParentTitle(CharSequence title, CharSequence shortTitle,
160             OnClickListener listener) {
161         mParentEntry = createBackStackEntry(title, shortTitle);
162         mParentClickListener = listener;
163         updateCrumbs();
164     }
165 
166     /**
167      * Sets a listener for clicks on the bread crumbs.  This will be called before
168      * the default click action is performed.
169      *
170      * @param listener The new listener to set.  Replaces any existing listener.
171      */
setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener)172     public void setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener) {
173         mOnBreadCrumbClickListener = listener;
174     }
175 
createBackStackEntry(CharSequence title, CharSequence shortTitle)176     private BackStackRecord createBackStackEntry(CharSequence title, CharSequence shortTitle) {
177         if (title == null) return null;
178 
179         final BackStackRecord entry = new BackStackRecord(
180                 (FragmentManagerImpl) mActivity.getFragmentManager());
181         entry.setBreadCrumbTitle(title);
182         entry.setBreadCrumbShortTitle(shortTitle);
183         return entry;
184     }
185 
186     /**
187      * Set a custom title for the bread crumbs.  This will be the first entry
188      * shown at the left, representing the root of the bread crumbs.  If the
189      * title is null, it will not be shown.
190      */
setTitle(CharSequence title, CharSequence shortTitle)191     public void setTitle(CharSequence title, CharSequence shortTitle) {
192         mTopEntry = createBackStackEntry(title, shortTitle);
193         updateCrumbs();
194     }
195 
196     @Override
onLayout(boolean changed, int l, int t, int r, int b)197     protected void onLayout(boolean changed, int l, int t, int r, int b) {
198         // Eventually we should implement our own layout of the views, rather than relying on
199         // a single linear layout.
200         final int childCount = getChildCount();
201         if (childCount == 0) {
202             return;
203         }
204 
205         final View child = getChildAt(0);
206 
207         final int childTop = mPaddingTop;
208         final int childBottom = mPaddingTop + child.getMeasuredHeight() - mPaddingBottom;
209 
210         int childLeft;
211         int childRight;
212 
213         final int layoutDirection = getLayoutDirection();
214         final int horizontalGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
215         switch (Gravity.getAbsoluteGravity(horizontalGravity, layoutDirection)) {
216             case Gravity.RIGHT:
217                 childRight = mRight - mLeft - mPaddingRight;
218                 childLeft = childRight - child.getMeasuredWidth();
219                 break;
220 
221             case Gravity.CENTER_HORIZONTAL:
222                 childLeft = mPaddingLeft + (mRight - mLeft - child.getMeasuredWidth()) / 2;
223                 childRight = childLeft + child.getMeasuredWidth();
224                 break;
225 
226             case Gravity.LEFT:
227             default:
228                 childLeft = mPaddingLeft;
229                 childRight = childLeft + child.getMeasuredWidth();
230                 break;
231         }
232 
233         if (childLeft < mPaddingLeft) {
234             childLeft = mPaddingLeft;
235         }
236 
237         if (childRight > mRight - mLeft - mPaddingRight) {
238             childRight = mRight - mLeft - mPaddingRight;
239         }
240 
241         child.layout(childLeft, childTop, childRight, childBottom);
242     }
243 
244     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)245     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
246         final int count = getChildCount();
247 
248         int maxHeight = 0;
249         int maxWidth = 0;
250         int measuredChildState = 0;
251 
252         // Find rightmost and bottom-most child
253         for (int i = 0; i < count; i++) {
254             final View child = getChildAt(i);
255             if (child.getVisibility() != GONE) {
256                 measureChild(child, widthMeasureSpec, heightMeasureSpec);
257                 maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
258                 maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
259                 measuredChildState = combineMeasuredStates(measuredChildState,
260                         child.getMeasuredState());
261             }
262         }
263 
264         // Account for padding too
265         maxWidth += mPaddingLeft + mPaddingRight;
266         maxHeight += mPaddingTop + mPaddingBottom;
267 
268         // Check against our minimum height and width
269         maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
270         maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
271 
272         setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, measuredChildState),
273                 resolveSizeAndState(maxHeight, heightMeasureSpec,
274                         measuredChildState<<MEASURED_HEIGHT_STATE_SHIFT));
275     }
276 
277     @Override
onBackStackChanged()278     public void onBackStackChanged() {
279         updateCrumbs();
280     }
281 
282     /**
283      * Returns the number of entries before the backstack, including the title of the current
284      * fragment and any custom parent title that was set.
285      */
getPreEntryCount()286     private int getPreEntryCount() {
287         return (mTopEntry != null ? 1 : 0) + (mParentEntry != null ? 1 : 0);
288     }
289 
290     /**
291      * Returns the pre-entry corresponding to the index. If there is a parent and a top entry
292      * set, parent has an index of zero and top entry has an index of 1. Returns null if the
293      * specified index doesn't exist or is null.
294      * @param index should not be more than {@link #getPreEntryCount()} - 1
295      */
getPreEntry(int index)296     private BackStackEntry getPreEntry(int index) {
297         // If there's a parent entry, then return that for zero'th item, else top entry.
298         if (mParentEntry != null) {
299             return index == 0 ? mParentEntry : mTopEntry;
300         } else {
301             return mTopEntry;
302         }
303     }
304 
updateCrumbs()305     void updateCrumbs() {
306         FragmentManager fm = mActivity.getFragmentManager();
307         int numEntries = fm.getBackStackEntryCount();
308         int numPreEntries = getPreEntryCount();
309         int numViews = mContainer.getChildCount();
310         for (int i = 0; i < numEntries + numPreEntries; i++) {
311             BackStackEntry bse = i < numPreEntries
312                     ? getPreEntry(i)
313                     : fm.getBackStackEntryAt(i - numPreEntries);
314             if (i < numViews) {
315                 View v = mContainer.getChildAt(i);
316                 Object tag = v.getTag();
317                 if (tag != bse) {
318                     for (int j = i; j < numViews; j++) {
319                         mContainer.removeViewAt(i);
320                     }
321                     numViews = i;
322                 }
323             }
324             if (i >= numViews) {
325                 final View item = mInflater.inflate(mLayoutResId, this, false);
326                 final TextView text = (TextView) item.findViewById(com.android.internal.R.id.title);
327                 text.setText(bse.getBreadCrumbTitle());
328                 text.setTag(bse);
329                 text.setTextColor(mTextColor);
330                 if (i == 0) {
331                     item.findViewById(com.android.internal.R.id.left_icon).setVisibility(View.GONE);
332                 }
333                 mContainer.addView(item);
334                 text.setOnClickListener(mOnClickListener);
335             }
336         }
337         int viewI = numEntries + numPreEntries;
338         numViews = mContainer.getChildCount();
339         while (numViews > viewI) {
340             mContainer.removeViewAt(numViews - 1);
341             numViews--;
342         }
343         // Adjust the visibility and availability of the bread crumbs and divider
344         for (int i = 0; i < numViews; i++) {
345             final View child = mContainer.getChildAt(i);
346             // Disable the last one
347             child.findViewById(com.android.internal.R.id.title).setEnabled(i < numViews - 1);
348             if (mMaxVisible > 0) {
349                 // Make only the last mMaxVisible crumbs visible
350                 child.setVisibility(i < numViews - mMaxVisible ? View.GONE : View.VISIBLE);
351                 final View leftIcon = child.findViewById(com.android.internal.R.id.left_icon);
352                 // Remove the divider for all but the last mMaxVisible - 1
353                 leftIcon.setVisibility(i > numViews - mMaxVisible && i != 0 ? View.VISIBLE
354                         : View.GONE);
355             }
356         }
357     }
358 
359     private OnClickListener mOnClickListener = new OnClickListener() {
360         public void onClick(View v) {
361             if (v.getTag() instanceof BackStackEntry) {
362                 BackStackEntry bse = (BackStackEntry) v.getTag();
363                 if (bse == mParentEntry) {
364                     if (mParentClickListener != null) {
365                         mParentClickListener.onClick(v);
366                     }
367                 } else {
368                     if (mOnBreadCrumbClickListener != null) {
369                         if (mOnBreadCrumbClickListener.onBreadCrumbClick(
370                                 bse == mTopEntry ? null : bse, 0)) {
371                             return;
372                         }
373                     }
374                     if (bse == mTopEntry) {
375                         // Pop everything off the back stack.
376                         mActivity.getFragmentManager().popBackStack();
377                     } else {
378                         mActivity.getFragmentManager().popBackStack(bse.getId(), 0);
379                     }
380                 }
381             }
382         }
383     };
384 }
385