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