blob: b810b893b704615088d03dfb243d9caeb224925c [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) {
84 this(context, attrs, android.R.style.Widget_FragmentBreadCrumbs);
85 }
86
87 public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyle) {
88 super(context, attrs, defStyle);
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -070089
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();
Dianne Hackbornc6669ca2010-09-16 01:33:24 -070097 }
98
99 /**
100 * Attach the bread crumbs to their activity. This must be called once
101 * when creating the bread crumbs.
102 */
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();
Dianne Hackborn8eb2e242010-11-01 12:31:24 -0700112 setLayoutTransition(new LayoutTransition());
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700113 }
114
115 /**
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800116 * 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.
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800118 */
119 public void setMaxVisible(int visibleCrumbs) {
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800120 if (visibleCrumbs < 1) {
121 throw new IllegalArgumentException("visibleCrumbs must be greater than zero");
122 }
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800123 mMaxVisible = visibleCrumbs;
124 }
125
126 /**
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800127 * 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 */
137 public void setParentTitle(CharSequence title, CharSequence shortTitle,
138 OnClickListener listener) {
139 mParentEntry = createBackStackEntry(title, shortTitle);
140 mParentClickListener = listener;
141 updateCrumbs();
142 }
143
Dianne Hackbornd94df452011-02-16 18:53:31 -0800144 /**
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 */
150 public void setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener) {
151 mOnBreadCrumbClickListener = listener;
152 }
153
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800154 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 /**
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700165 * 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 */
169 public void setTitle(CharSequence title, CharSequence shortTitle) {
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800170 mTopEntry = createBackStackEntry(title, shortTitle);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700171 updateCrumbs();
172 }
173
174 @Override
175 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -0700176 // Eventually we should implement our own layout of the views, rather than relying on
177 // a single linear layout.
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700178 final int childCount = getChildCount();
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -0700179 if (childCount == 0) {
180 return;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700181 }
Fabrice Di Meglio3cc10f42012-10-10 19:11:47 -0700182
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);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700220 }
221
222 @Override
223 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
224 final int count = getChildCount();
225
226 int maxHeight = 0;
227 int maxWidth = 0;
Dianne Hackborn189ee182010-12-02 21:48:53 -0800228 int measuredChildState = 0;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700229
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());
Dianne Hackborn189ee182010-12-02 21:48:53 -0800237 measuredChildState = combineMeasuredStates(measuredChildState,
238 child.getMeasuredState());
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700239 }
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
Dianne Hackborn189ee182010-12-02 21:48:53 -0800250 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, measuredChildState),
251 resolveSizeAndState(maxHeight, heightMeasureSpec,
252 measuredChildState<<MEASURED_HEIGHT_STATE_SHIFT));
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700253 }
254
255 @Override
256 public void onBackStackChanged() {
257 updateCrumbs();
258 }
259
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800260 /**
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 */
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 */
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
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700283 void updateCrumbs() {
284 FragmentManager fm = mActivity.getFragmentManager();
Dianne Hackborn327fbd22011-01-17 14:38:50 -0800285 int numEntries = fm.getBackStackEntryCount();
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800286 int numPreEntries = getPreEntryCount();
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700287 int numViews = mContainer.getChildCount();
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800288 for (int i = 0; i < numEntries + numPreEntries; i++) {
289 BackStackEntry bse = i < numPreEntries
290 ? getPreEntry(i)
Dianne Hackborn327fbd22011-01-17 14:38:50 -0800291 : fm.getBackStackEntryAt(i - numPreEntries);
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800292 if (i < numViews) {
293 View v = mContainer.getChildAt(i);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700294 Object tag = v.getTag();
295 if (tag != bse) {
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800296 for (int j = i; j < numViews; j++) {
297 mContainer.removeViewAt(i);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700298 }
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800299 numViews = i;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700300 }
301 }
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800302 if (i >= numViews) {
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800303 final View item = mInflater.inflate(
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700304 com.android.internal.R.layout.fragment_bread_crumb_item,
305 this, false);
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800306 final TextView text = (TextView) item.findViewById(com.android.internal.R.id.title);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700307 text.setText(bse.getBreadCrumbTitle());
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800308 text.setTag(bse);
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800309 if (i == 0) {
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800310 item.findViewById(com.android.internal.R.id.left_icon).setVisibility(View.GONE);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700311 }
312 mContainer.addView(item);
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800313 text.setOnClickListener(mOnClickListener);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700314 }
315 }
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800316 int viewI = numEntries + numPreEntries;
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700317 numViews = mContainer.getChildCount();
318 while (numViews > viewI) {
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800319 mContainer.removeViewAt(numViews - 1);
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700320 numViews--;
321 }
Amith Yamasani3c9f5192010-12-08 16:48:31 -0800322 // 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 }
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700336 }
Amith Yamasanidcfb9f72010-09-21 14:22:09 -0700337
338 private OnClickListener mOnClickListener = new OnClickListener() {
339 public void onClick(View v) {
340 if (v.getTag() instanceof BackStackEntry) {
341 BackStackEntry bse = (BackStackEntry) v.getTag();
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800342 if (bse == mParentEntry) {
343 if (mParentClickListener != null) {
344 mParentClickListener.onClick(v);
345 }
346 } else {
Dianne Hackbornd94df452011-02-16 18:53:31 -0800347 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 }
Amith Yamasanic9ecb732010-12-14 14:23:21 -0800359 }
Amith Yamasanidcfb9f72010-09-21 14:22:09 -0700360 }
361 }
362 };
Dianne Hackbornc6669ca2010-09-16 01:33:24 -0700363}