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  * @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          */
     81         public boolean onBreadCrumbClick(BackStackEntry backStack, int flags);
     82     }
     83 
     84     public FragmentBreadCrumbs(Context context) {
     85         this(context, null);
     86     }
     87 
     88     public FragmentBreadCrumbs(Context context, AttributeSet attrs) {
     89         this(context, attrs, com.android.internal.R.attr.fragmentBreadCrumbsStyle);
     90     }
     91 
     92     public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr) {
     93         this(context, attrs, defStyleAttr, 0);
     94     }
     95 
     96     /**
     97      * @hide
     98      */
     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      */
    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      */
    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      */
    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      */
    169     public void setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener) {
    170         mOnBreadCrumbClickListener = listener;
    171     }
    172 
    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      */
    188     public void setTitle(CharSequence title, CharSequence shortTitle) {
    189         mTopEntry = createBackStackEntry(title, shortTitle);
    190         updateCrumbs();
    191     }
    192 
    193     @Override
    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
    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
    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      */
    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      */
    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 
    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