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