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