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.util.AttributeSet; 23 import android.view.LayoutInflater; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.widget.LinearLayout; 27 import android.widget.TextView; 28 29 /** 30 * Helper class for showing "bread crumbs" representing the fragment 31 * stack in an activity. This is intended to be used with 32 * {@link ActionBar#setCustomView(View) 33 * ActionBar.setCustomView(View)} to place the bread crumbs in 34 * the action bar. 35 * 36 * <p>The default style for this view is 37 * {@link android.R.style#Widget_FragmentBreadCrumbs}. 38 */ 39 public class FragmentBreadCrumbs extends ViewGroup 40 implements FragmentManager.OnBackStackChangedListener { 41 Activity mActivity; 42 LayoutInflater mInflater; 43 LinearLayout mContainer; 44 int mMaxVisible = -1; 45 46 // Hahah 47 BackStackRecord mTopEntry; 48 BackStackRecord mParentEntry; 49 50 /** Listener to inform when a parent entry is clicked */ 51 private OnClickListener mParentClickListener; 52 53 private OnBreadCrumbClickListener mOnBreadCrumbClickListener; 54 55 /** 56 * Interface to intercept clicks on the bread crumbs. 57 */ 58 public interface OnBreadCrumbClickListener { 59 /** 60 * Called when a bread crumb is clicked. 61 * 62 * @param backStack The BackStackEntry whose bread crumb was clicked. 63 * May be null, if this bread crumb is for the root of the back stack. 64 * @param flags Additional information about the entry. Currently 65 * always 0. 66 * 67 * @return Return true to consume this click. Return to false to allow 68 * the default action (popping back stack to this entry) to occur. 69 */ 70 public boolean onBreadCrumbClick(BackStackEntry backStack, int flags); 71 } 72 73 public FragmentBreadCrumbs(Context context) { 74 this(context, null); 75 } 76 77 public FragmentBreadCrumbs(Context context, AttributeSet attrs) { 78 this(context, attrs, android.R.style.Widget_FragmentBreadCrumbs); 79 } 80 81 public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyle) { 82 super(context, attrs, defStyle); 83 } 84 85 /** 86 * Attach the bread crumbs to their activity. This must be called once 87 * when creating the bread crumbs. 88 */ 89 public void setActivity(Activity a) { 90 mActivity = a; 91 mInflater = (LayoutInflater)a.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 92 mContainer = (LinearLayout)mInflater.inflate( 93 com.android.internal.R.layout.fragment_bread_crumbs, 94 this, false); 95 addView(mContainer); 96 a.getFragmentManager().addOnBackStackChangedListener(this); 97 updateCrumbs(); 98 setLayoutTransition(new LayoutTransition()); 99 } 100 101 /** 102 * The maximum number of breadcrumbs to show. Older fragment headers will be hidden from view. 103 * @param visibleCrumbs the number of visible breadcrumbs. This should be greater than zero. 104 */ 105 public void setMaxVisible(int visibleCrumbs) { 106 if (visibleCrumbs < 1) { 107 throw new IllegalArgumentException("visibleCrumbs must be greater than zero"); 108 } 109 mMaxVisible = visibleCrumbs; 110 } 111 112 /** 113 * Inserts an optional parent entry at the first position in the breadcrumbs. Selecting this 114 * entry will result in a call to the specified listener's 115 * {@link android.view.View.OnClickListener#onClick(View)} 116 * method. 117 * 118 * @param title the title for the parent entry 119 * @param shortTitle the short title for the parent entry 120 * @param listener the {@link android.view.View.OnClickListener} to be called when clicked. 121 * A null will result in no action being taken when the parent entry is clicked. 122 */ 123 public void setParentTitle(CharSequence title, CharSequence shortTitle, 124 OnClickListener listener) { 125 mParentEntry = createBackStackEntry(title, shortTitle); 126 mParentClickListener = listener; 127 updateCrumbs(); 128 } 129 130 /** 131 * Sets a listener for clicks on the bread crumbs. This will be called before 132 * the default click action is performed. 133 * 134 * @param listener The new listener to set. Replaces any existing listener. 135 */ 136 public void setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener) { 137 mOnBreadCrumbClickListener = listener; 138 } 139 140 private BackStackRecord createBackStackEntry(CharSequence title, CharSequence shortTitle) { 141 if (title == null) return null; 142 143 final BackStackRecord entry = new BackStackRecord( 144 (FragmentManagerImpl) mActivity.getFragmentManager()); 145 entry.setBreadCrumbTitle(title); 146 entry.setBreadCrumbShortTitle(shortTitle); 147 return entry; 148 } 149 150 /** 151 * Set a custom title for the bread crumbs. This will be the first entry 152 * shown at the left, representing the root of the bread crumbs. If the 153 * title is null, it will not be shown. 154 */ 155 public void setTitle(CharSequence title, CharSequence shortTitle) { 156 mTopEntry = createBackStackEntry(title, shortTitle); 157 updateCrumbs(); 158 } 159 160 @Override 161 protected void onLayout(boolean changed, int l, int t, int r, int b) { 162 // Eventually we should implement our own layout of the views, 163 // rather than relying on a linear layout. 164 final int childCount = getChildCount(); 165 for (int i = 0; i < childCount; i++) { 166 final View child = getChildAt(i); 167 168 int childRight = mPaddingLeft + child.getMeasuredWidth() - mPaddingRight; 169 int childBottom = mPaddingTop + child.getMeasuredHeight() - mPaddingBottom; 170 child.layout(mPaddingLeft, mPaddingTop, childRight, childBottom); 171 } 172 } 173 174 @Override 175 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 176 final int count = getChildCount(); 177 178 int maxHeight = 0; 179 int maxWidth = 0; 180 int measuredChildState = 0; 181 182 // Find rightmost and bottom-most child 183 for (int i = 0; i < count; i++) { 184 final View child = getChildAt(i); 185 if (child.getVisibility() != GONE) { 186 measureChild(child, widthMeasureSpec, heightMeasureSpec); 187 maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); 188 maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); 189 measuredChildState = combineMeasuredStates(measuredChildState, 190 child.getMeasuredState()); 191 } 192 } 193 194 // Account for padding too 195 maxWidth += mPaddingLeft + mPaddingRight; 196 maxHeight += mPaddingTop + mPaddingBottom; 197 198 // Check against our minimum height and width 199 maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); 200 maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); 201 202 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, measuredChildState), 203 resolveSizeAndState(maxHeight, heightMeasureSpec, 204 measuredChildState<<MEASURED_HEIGHT_STATE_SHIFT)); 205 } 206 207 @Override 208 public void onBackStackChanged() { 209 updateCrumbs(); 210 } 211 212 /** 213 * Returns the number of entries before the backstack, including the title of the current 214 * fragment and any custom parent title that was set. 215 */ 216 private int getPreEntryCount() { 217 return (mTopEntry != null ? 1 : 0) + (mParentEntry != null ? 1 : 0); 218 } 219 220 /** 221 * Returns the pre-entry corresponding to the index. If there is a parent and a top entry 222 * set, parent has an index of zero and top entry has an index of 1. Returns null if the 223 * specified index doesn't exist or is null. 224 * @param index should not be more than {@link #getPreEntryCount()} - 1 225 */ 226 private BackStackEntry getPreEntry(int index) { 227 // If there's a parent entry, then return that for zero'th item, else top entry. 228 if (mParentEntry != null) { 229 return index == 0 ? mParentEntry : mTopEntry; 230 } else { 231 return mTopEntry; 232 } 233 } 234 235 void updateCrumbs() { 236 FragmentManager fm = mActivity.getFragmentManager(); 237 int numEntries = fm.getBackStackEntryCount(); 238 int numPreEntries = getPreEntryCount(); 239 int numViews = mContainer.getChildCount(); 240 for (int i = 0; i < numEntries + numPreEntries; i++) { 241 BackStackEntry bse = i < numPreEntries 242 ? getPreEntry(i) 243 : fm.getBackStackEntryAt(i - numPreEntries); 244 if (i < numViews) { 245 View v = mContainer.getChildAt(i); 246 Object tag = v.getTag(); 247 if (tag != bse) { 248 for (int j = i; j < numViews; j++) { 249 mContainer.removeViewAt(i); 250 } 251 numViews = i; 252 } 253 } 254 if (i >= numViews) { 255 final View item = mInflater.inflate( 256 com.android.internal.R.layout.fragment_bread_crumb_item, 257 this, false); 258 final TextView text = (TextView) item.findViewById(com.android.internal.R.id.title); 259 text.setText(bse.getBreadCrumbTitle()); 260 text.setTag(bse); 261 if (i == 0) { 262 item.findViewById(com.android.internal.R.id.left_icon).setVisibility(View.GONE); 263 } 264 mContainer.addView(item); 265 text.setOnClickListener(mOnClickListener); 266 } 267 } 268 int viewI = numEntries + numPreEntries; 269 numViews = mContainer.getChildCount(); 270 while (numViews > viewI) { 271 mContainer.removeViewAt(numViews - 1); 272 numViews--; 273 } 274 // Adjust the visibility and availability of the bread crumbs and divider 275 for (int i = 0; i < numViews; i++) { 276 final View child = mContainer.getChildAt(i); 277 // Disable the last one 278 child.findViewById(com.android.internal.R.id.title).setEnabled(i < numViews - 1); 279 if (mMaxVisible > 0) { 280 // Make only the last mMaxVisible crumbs visible 281 child.setVisibility(i < numViews - mMaxVisible ? View.GONE : View.VISIBLE); 282 final View leftIcon = child.findViewById(com.android.internal.R.id.left_icon); 283 // Remove the divider for all but the last mMaxVisible - 1 284 leftIcon.setVisibility(i > numViews - mMaxVisible && i != 0 ? View.VISIBLE 285 : View.GONE); 286 } 287 } 288 } 289 290 private OnClickListener mOnClickListener = new OnClickListener() { 291 public void onClick(View v) { 292 if (v.getTag() instanceof BackStackEntry) { 293 BackStackEntry bse = (BackStackEntry) v.getTag(); 294 if (bse == mParentEntry) { 295 if (mParentClickListener != null) { 296 mParentClickListener.onClick(v); 297 } 298 } else { 299 if (mOnBreadCrumbClickListener != null) { 300 if (mOnBreadCrumbClickListener.onBreadCrumbClick( 301 bse == mTopEntry ? null : bse, 0)) { 302 return; 303 } 304 } 305 if (bse == mTopEntry) { 306 // Pop everything off the back stack. 307 mActivity.getFragmentManager().popBackStack(); 308 } else { 309 mActivity.getFragmentManager().popBackStack(bse.getId(), 0); 310 } 311 } 312 } 313 } 314 }; 315 } 316