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