Home | History | Annotate | Download | only in app
      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          */
     76         public boolean onBreadCrumbClick(BackStackEntry backStack, int flags);
     77     }
     78 
     79     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);
     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      */
    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      */
    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      */
    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      */
    150     public void setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener) {
    151         mOnBreadCrumbClickListener = listener;
    152     }
    153 
    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      */
    169     public void setTitle(CharSequence title, CharSequence shortTitle) {
    170         mTopEntry = createBackStackEntry(title, shortTitle);
    171         updateCrumbs();
    172     }
    173 
    174     @Override
    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
    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
    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      */
    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 
    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