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.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