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