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
17package android.app;
18
19import android.animation.LayoutTransition;
20import android.app.FragmentManager.BackStackEntry;
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.util.AttributeSet;
24import android.view.Gravity;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.LinearLayout;
29import 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 */
41public 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