blob: e4de7af7f92c2fe1f7652d60fa5c4400b15b5efa [file] [log] [blame]
Dianne Hackbornc6669ca2010-09-16 01:33:24 -07001/*
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
17package android.app;
18
Dianne Hackborn8eb2e242010-11-01 12:31:24 -070019import android.animation.LayoutTransition;
Amith Yamasanidcfb9f72010-09-21 14:22:09 -070020import android.app.FragmentManager.BackStackEntry;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070021import android.content.Context;
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -070022import android.content.res.TypedArray;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070023import android.util.AttributeSet;
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -070024import android.view.Gravity;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070025import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.LinearLayout;
29import 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
Adam Powell1264c332011-01-20 12:08:13 -080034 * {@link ActionBar#setCustomView(View)
35 * ActionBar.setCustomView(View)} to place the bread crumbs in
36 * the action bar.
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070037 *
38 * <p>The default style for this view is
39 * {@link android.R.style#Widget_FragmentBreadCrumbs}.
40 */
41public class FragmentBreadCrumbs extends ViewGroup
42 implements FragmentManager.OnBackStackChangedListener {
43 Activity mActivity;
44 LayoutInflater mInflater;
45 LinearLayout mContainer;
Amith Yamasani3c9f5192010-12-08 16:48:31 -080046 int mMaxVisible = -1;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070047
48 // Hahah
49 BackStackRecord mTopEntry;
Amith Yamasanic9ecb732010-12-14 14:23:21 -080050 BackStackRecord mParentEntry;
51
52 /** Listener to inform when a parent entry is clicked */
53 private OnClickListener mParentClickListener;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070054
Dianne Hackbornd94df452011-02-16 18:53:31 -080055 private OnBreadCrumbClickListener mOnBreadCrumbClickListener;
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -070056
57 private int mGravity;
58
59 private static final int DEFAULT_GRAVITY = Gravity.START | Gravity.CENTER_VERTICAL;
60
Dianne Hackbornd94df452011-02-16 18:53:31 -080061 /**
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 */
76 public boolean onBreadCrumbClick(BackStackEntry backStack, int flags);
77 }
78
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070079 public FragmentBreadCrumbs(Context context) {
80 this(context, null);
81 }
82
83 public FragmentBreadCrumbs(Context context, AttributeSet attrs) {
Alan Viveretteaaca5d82013-09-10 15:04:10 -070084 this(context, attrs, com.android.internal.R.attr.fragmentBreadCrumbsStyle);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070085 }
86
Alan Viverette617feb92013-09-09 18:09:13 -070087 public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr) {
88 this(context, attrs, defStyleAttr, 0);
89 }
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -070090
Alan Viverette617feb92013-09-09 18:09:13 -070091 public FragmentBreadCrumbs(
92 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
93 super(context, attrs, defStyleAttr, defStyleRes);
94
95 final TypedArray a = context.obtainStyledAttributes(attrs,
96 com.android.internal.R.styleable.FragmentBreadCrumbs, defStyleAttr, defStyleRes);
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -070097
98 mGravity = a.getInt(com.android.internal.R.styleable.FragmentBreadCrumbs_gravity,
99 DEFAULT_GRAVITY);
100
101 a.recycle();
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700102 }
103
104 /**
105 * Attach the bread crumbs to their activity. This must be called once
106 * when creating the bread crumbs.
107 */
108 public void setActivity(Activity a) {
109 mActivity = a;
110 mInflater = (LayoutInflater)a.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
111 mContainer = (LinearLayout)mInflater.inflate(
112 com.android.internal.R.layout.fragment_bread_crumbs,
113 this, false);
114 addView(mContainer);
115 a.getFragmentManager().addOnBackStackChangedListener(this);
116 updateCrumbs();
Dianne Hackborn8eb2e242010-11-01 12:31:24 -0700117 setLayoutTransition(new LayoutTransition());
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700118 }
119
120 /**
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800121 * The maximum number of breadcrumbs to show. Older fragment headers will be hidden from view.
122 * @param visibleCrumbs the number of visible breadcrumbs. This should be greater than zero.
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800123 */
124 public void setMaxVisible(int visibleCrumbs) {
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800125 if (visibleCrumbs < 1) {
126 throw new IllegalArgumentException("visibleCrumbs must be greater than zero");
127 }
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800128 mMaxVisible = visibleCrumbs;
129 }
130
131 /**
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800132 * Inserts an optional parent entry at the first position in the breadcrumbs. Selecting this
133 * entry will result in a call to the specified listener's
134 * {@link android.view.View.OnClickListener#onClick(View)}
135 * method.
136 *
137 * @param title the title for the parent entry
138 * @param shortTitle the short title for the parent entry
139 * @param listener the {@link android.view.View.OnClickListener} to be called when clicked.
140 * A null will result in no action being taken when the parent entry is clicked.
141 */
142 public void setParentTitle(CharSequence title, CharSequence shortTitle,
143 OnClickListener listener) {
144 mParentEntry = createBackStackEntry(title, shortTitle);
145 mParentClickListener = listener;
146 updateCrumbs();
147 }
148
Dianne Hackbornd94df452011-02-16 18:53:31 -0800149 /**
150 * Sets a listener for clicks on the bread crumbs. This will be called before
151 * the default click action is performed.
152 *
153 * @param listener The new listener to set. Replaces any existing listener.
154 */
155 public void setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener) {
156 mOnBreadCrumbClickListener = listener;
157 }
158
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800159 private BackStackRecord createBackStackEntry(CharSequence title, CharSequence shortTitle) {
160 if (title == null) return null;
161
162 final BackStackRecord entry = new BackStackRecord(
163 (FragmentManagerImpl) mActivity.getFragmentManager());
164 entry.setBreadCrumbTitle(title);
165 entry.setBreadCrumbShortTitle(shortTitle);
166 return entry;
167 }
168
169 /**
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700170 * Set a custom title for the bread crumbs. This will be the first entry
171 * shown at the left, representing the root of the bread crumbs. If the
172 * title is null, it will not be shown.
173 */
174 public void setTitle(CharSequence title, CharSequence shortTitle) {
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800175 mTopEntry = createBackStackEntry(title, shortTitle);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700176 updateCrumbs();
177 }
178
179 @Override
180 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -0700181 // Eventually we should implement our own layout of the views, rather than relying on
182 // a single linear layout.
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700183 final int childCount = getChildCount();
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -0700184 if (childCount == 0) {
185 return;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700186 }
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -0700187
188 final View child = getChildAt(0);
189
190 final int childTop = mPaddingTop;
191 final int childBottom = mPaddingTop + child.getMeasuredHeight() - mPaddingBottom;
192
193 int childLeft;
194 int childRight;
195
196 final int layoutDirection = getLayoutDirection();
197 final int horizontalGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
198 switch (Gravity.getAbsoluteGravity(horizontalGravity, layoutDirection)) {
199 case Gravity.RIGHT:
200 childRight = mRight - mLeft - mPaddingRight;
201 childLeft = childRight - child.getMeasuredWidth();
202 break;
203
204 case Gravity.CENTER_HORIZONTAL:
205 childLeft = mPaddingLeft + (mRight - mLeft - child.getMeasuredWidth()) / 2;
206 childRight = childLeft + child.getMeasuredWidth();
207 break;
208
209 case Gravity.LEFT:
210 default:
211 childLeft = mPaddingLeft;
212 childRight = childLeft + child.getMeasuredWidth();
213 break;
214 }
215
216 if (childLeft < mPaddingLeft) {
217 childLeft = mPaddingLeft;
218 }
219
220 if (childRight > mRight - mLeft - mPaddingRight) {
221 childRight = mRight - mLeft - mPaddingRight;
222 }
223
224 child.layout(childLeft, childTop, childRight, childBottom);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700225 }
226
227 @Override
228 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
229 final int count = getChildCount();
230
231 int maxHeight = 0;
232 int maxWidth = 0;
Dianne Hackborn189ee182010-12-02 21:48:53 -0800233 int measuredChildState = 0;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700234
235 // Find rightmost and bottom-most child
236 for (int i = 0; i < count; i++) {
237 final View child = getChildAt(i);
238 if (child.getVisibility() != GONE) {
239 measureChild(child, widthMeasureSpec, heightMeasureSpec);
240 maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
241 maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
Dianne Hackborn189ee182010-12-02 21:48:53 -0800242 measuredChildState = combineMeasuredStates(measuredChildState,
243 child.getMeasuredState());
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700244 }
245 }
246
247 // Account for padding too
248 maxWidth += mPaddingLeft + mPaddingRight;
249 maxHeight += mPaddingTop + mPaddingBottom;
250
251 // Check against our minimum height and width
252 maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
253 maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
254
Dianne Hackborn189ee182010-12-02 21:48:53 -0800255 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, measuredChildState),
256 resolveSizeAndState(maxHeight, heightMeasureSpec,
257 measuredChildState<<MEASURED_HEIGHT_STATE_SHIFT));
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700258 }
259
260 @Override
261 public void onBackStackChanged() {
262 updateCrumbs();
263 }
264
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800265 /**
266 * Returns the number of entries before the backstack, including the title of the current
267 * fragment and any custom parent title that was set.
268 */
269 private int getPreEntryCount() {
270 return (mTopEntry != null ? 1 : 0) + (mParentEntry != null ? 1 : 0);
271 }
272
273 /**
274 * Returns the pre-entry corresponding to the index. If there is a parent and a top entry
275 * set, parent has an index of zero and top entry has an index of 1. Returns null if the
276 * specified index doesn't exist or is null.
277 * @param index should not be more than {@link #getPreEntryCount()} - 1
278 */
279 private BackStackEntry getPreEntry(int index) {
280 // If there's a parent entry, then return that for zero'th item, else top entry.
281 if (mParentEntry != null) {
282 return index == 0 ? mParentEntry : mTopEntry;
283 } else {
284 return mTopEntry;
285 }
286 }
287
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700288 void updateCrumbs() {
289 FragmentManager fm = mActivity.getFragmentManager();
Dianne Hackborn327fbd22011-01-17 14:38:50 -0800290 int numEntries = fm.getBackStackEntryCount();
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800291 int numPreEntries = getPreEntryCount();
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700292 int numViews = mContainer.getChildCount();
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800293 for (int i = 0; i < numEntries + numPreEntries; i++) {
294 BackStackEntry bse = i < numPreEntries
295 ? getPreEntry(i)
Dianne Hackborn327fbd22011-01-17 14:38:50 -0800296 : fm.getBackStackEntryAt(i - numPreEntries);
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800297 if (i < numViews) {
298 View v = mContainer.getChildAt(i);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700299 Object tag = v.getTag();
300 if (tag != bse) {
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800301 for (int j = i; j < numViews; j++) {
302 mContainer.removeViewAt(i);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700303 }
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800304 numViews = i;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700305 }
306 }
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800307 if (i >= numViews) {
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800308 final View item = mInflater.inflate(
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700309 com.android.internal.R.layout.fragment_bread_crumb_item,
310 this, false);
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800311 final TextView text = (TextView) item.findViewById(com.android.internal.R.id.title);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700312 text.setText(bse.getBreadCrumbTitle());
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800313 text.setTag(bse);
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800314 if (i == 0) {
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800315 item.findViewById(com.android.internal.R.id.left_icon).setVisibility(View.GONE);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700316 }
317 mContainer.addView(item);
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800318 text.setOnClickListener(mOnClickListener);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700319 }
320 }
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800321 int viewI = numEntries + numPreEntries;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700322 numViews = mContainer.getChildCount();
323 while (numViews > viewI) {
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800324 mContainer.removeViewAt(numViews - 1);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700325 numViews--;
326 }
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800327 // Adjust the visibility and availability of the bread crumbs and divider
328 for (int i = 0; i < numViews; i++) {
329 final View child = mContainer.getChildAt(i);
330 // Disable the last one
331 child.findViewById(com.android.internal.R.id.title).setEnabled(i < numViews - 1);
332 if (mMaxVisible > 0) {
333 // Make only the last mMaxVisible crumbs visible
334 child.setVisibility(i < numViews - mMaxVisible ? View.GONE : View.VISIBLE);
335 final View leftIcon = child.findViewById(com.android.internal.R.id.left_icon);
336 // Remove the divider for all but the last mMaxVisible - 1
337 leftIcon.setVisibility(i > numViews - mMaxVisible && i != 0 ? View.VISIBLE
338 : View.GONE);
339 }
340 }
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700341 }
Amith Yamasanidcfb9f72010-09-21 14:22:09 -0700342
343 private OnClickListener mOnClickListener = new OnClickListener() {
344 public void onClick(View v) {
345 if (v.getTag() instanceof BackStackEntry) {
346 BackStackEntry bse = (BackStackEntry) v.getTag();
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800347 if (bse == mParentEntry) {
348 if (mParentClickListener != null) {
349 mParentClickListener.onClick(v);
350 }
351 } else {
Dianne Hackbornd94df452011-02-16 18:53:31 -0800352 if (mOnBreadCrumbClickListener != null) {
353 if (mOnBreadCrumbClickListener.onBreadCrumbClick(
354 bse == mTopEntry ? null : bse, 0)) {
355 return;
356 }
357 }
358 if (bse == mTopEntry) {
359 // Pop everything off the back stack.
360 mActivity.getFragmentManager().popBackStack();
361 } else {
362 mActivity.getFragmentManager().popBackStack(bse.getId(), 0);
363 }
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800364 }
Amith Yamasanidcfb9f72010-09-21 14:22:09 -0700365 }
366 }
367 };
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700368}