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 *
41 * @deprecated This widget is no longer supported.
42 */
43@Deprecated
44public 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     * @deprecated This widget is no longer supported.
70     */
71    @Deprecated
72    public interface OnBreadCrumbClickListener {
73        /**
74         * Called when a bread crumb is clicked.
75         *
76         * @param backStack The BackStackEntry whose bread crumb was clicked.
77         * May be null, if this bread crumb is for the root of the back stack.
78         * @param flags Additional information about the entry.  Currently
79         * always 0.
80         *
81         * @return Return true to consume this click.  Return to false to allow
82         * the default action (popping back stack to this entry) to occur.
83         */
84        public boolean onBreadCrumbClick(BackStackEntry backStack, int flags);
85    }
86
87    public FragmentBreadCrumbs(Context context) {
88        this(context, null);
89    }
90
91    public FragmentBreadCrumbs(Context context, AttributeSet attrs) {
92        this(context, attrs, com.android.internal.R.attr.fragmentBreadCrumbsStyle);
93    }
94
95    public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr) {
96        this(context, attrs, defStyleAttr, 0);
97    }
98
99    /**
100     * @hide
101     */
102    public FragmentBreadCrumbs(
103            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
104        super(context, attrs, defStyleAttr, defStyleRes);
105
106        final TypedArray a = context.obtainStyledAttributes(attrs,
107                com.android.internal.R.styleable.FragmentBreadCrumbs, defStyleAttr, defStyleRes);
108
109        mGravity = a.getInt(com.android.internal.R.styleable.FragmentBreadCrumbs_gravity,
110                DEFAULT_GRAVITY);
111        mLayoutResId = a.getResourceId(
112                com.android.internal.R.styleable.FragmentBreadCrumbs_itemLayout,
113                com.android.internal.R.layout.fragment_bread_crumb_item);
114        mTextColor = a.getColor(
115                com.android.internal.R.styleable.FragmentBreadCrumbs_itemColor,
116                0);
117
118        a.recycle();
119    }
120
121    /**
122     * Attach the bread crumbs to their activity.  This must be called once
123     * when creating the bread crumbs.
124     */
125    public void setActivity(Activity a) {
126        mActivity = a;
127        mInflater = (LayoutInflater)a.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
128        mContainer = (LinearLayout)mInflater.inflate(
129                com.android.internal.R.layout.fragment_bread_crumbs,
130                this, false);
131        addView(mContainer);
132        a.getFragmentManager().addOnBackStackChangedListener(this);
133        updateCrumbs();
134        setLayoutTransition(new LayoutTransition());
135    }
136
137    /**
138     * The maximum number of breadcrumbs to show. Older fragment headers will be hidden from view.
139     * @param visibleCrumbs the number of visible breadcrumbs. This should be greater than zero.
140     */
141    public void setMaxVisible(int visibleCrumbs) {
142        if (visibleCrumbs < 1) {
143            throw new IllegalArgumentException("visibleCrumbs must be greater than zero");
144        }
145        mMaxVisible = visibleCrumbs;
146    }
147
148    /**
149     * Inserts an optional parent entry at the first position in the breadcrumbs. Selecting this
150     * entry will result in a call to the specified listener's
151     * {@link android.view.View.OnClickListener#onClick(View)}
152     * method.
153     *
154     * @param title the title for the parent entry
155     * @param shortTitle the short title for the parent entry
156     * @param listener the {@link android.view.View.OnClickListener} to be called when clicked.
157     * A null will result in no action being taken when the parent entry is clicked.
158     */
159    public void setParentTitle(CharSequence title, CharSequence shortTitle,
160            OnClickListener listener) {
161        mParentEntry = createBackStackEntry(title, shortTitle);
162        mParentClickListener = listener;
163        updateCrumbs();
164    }
165
166    /**
167     * Sets a listener for clicks on the bread crumbs.  This will be called before
168     * the default click action is performed.
169     *
170     * @param listener The new listener to set.  Replaces any existing listener.
171     */
172    public void setOnBreadCrumbClickListener(OnBreadCrumbClickListener listener) {
173        mOnBreadCrumbClickListener = listener;
174    }
175
176    private BackStackRecord createBackStackEntry(CharSequence title, CharSequence shortTitle) {
177        if (title == null) return null;
178
179        final BackStackRecord entry = new BackStackRecord(
180                (FragmentManagerImpl) mActivity.getFragmentManager());
181        entry.setBreadCrumbTitle(title);
182        entry.setBreadCrumbShortTitle(shortTitle);
183        return entry;
184    }
185
186    /**
187     * Set a custom title for the bread crumbs.  This will be the first entry
188     * shown at the left, representing the root of the bread crumbs.  If the
189     * title is null, it will not be shown.
190     */
191    public void setTitle(CharSequence title, CharSequence shortTitle) {
192        mTopEntry = createBackStackEntry(title, shortTitle);
193        updateCrumbs();
194    }
195
196    @Override
197    protected void onLayout(boolean changed, int l, int t, int r, int b) {
198        // Eventually we should implement our own layout of the views, rather than relying on
199        // a single linear layout.
200        final int childCount = getChildCount();
201        if (childCount == 0) {
202            return;
203        }
204
205        final View child = getChildAt(0);
206
207        final int childTop = mPaddingTop;
208        final int childBottom = mPaddingTop + child.getMeasuredHeight() - mPaddingBottom;
209
210        int childLeft;
211        int childRight;
212
213        final int layoutDirection = getLayoutDirection();
214        final int horizontalGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
215        switch (Gravity.getAbsoluteGravity(horizontalGravity, layoutDirection)) {
216            case Gravity.RIGHT:
217                childRight = mRight - mLeft - mPaddingRight;
218                childLeft = childRight - child.getMeasuredWidth();
219                break;
220
221            case Gravity.CENTER_HORIZONTAL:
222                childLeft = mPaddingLeft + (mRight - mLeft - child.getMeasuredWidth()) / 2;
223                childRight = childLeft + child.getMeasuredWidth();
224                break;
225
226            case Gravity.LEFT:
227            default:
228                childLeft = mPaddingLeft;
229                childRight = childLeft + child.getMeasuredWidth();
230                break;
231        }
232
233        if (childLeft < mPaddingLeft) {
234            childLeft = mPaddingLeft;
235        }
236
237        if (childRight > mRight - mLeft - mPaddingRight) {
238            childRight = mRight - mLeft - mPaddingRight;
239        }
240
241        child.layout(childLeft, childTop, childRight, childBottom);
242    }
243
244    @Override
245    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
246        final int count = getChildCount();
247
248        int maxHeight = 0;
249        int maxWidth = 0;
250        int measuredChildState = 0;
251
252        // Find rightmost and bottom-most child
253        for (int i = 0; i < count; i++) {
254            final View child = getChildAt(i);
255            if (child.getVisibility() != GONE) {
256                measureChild(child, widthMeasureSpec, heightMeasureSpec);
257                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
258                maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
259                measuredChildState = combineMeasuredStates(measuredChildState,
260                        child.getMeasuredState());
261            }
262        }
263
264        // Account for padding too
265        maxWidth += mPaddingLeft + mPaddingRight;
266        maxHeight += mPaddingTop + mPaddingBottom;
267
268        // Check against our minimum height and width
269        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
270        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
271
272        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, measuredChildState),
273                resolveSizeAndState(maxHeight, heightMeasureSpec,
274                        measuredChildState<<MEASURED_HEIGHT_STATE_SHIFT));
275    }
276
277    @Override
278    public void onBackStackChanged() {
279        updateCrumbs();
280    }
281
282    /**
283     * Returns the number of entries before the backstack, including the title of the current
284     * fragment and any custom parent title that was set.
285     */
286    private int getPreEntryCount() {
287        return (mTopEntry != null ? 1 : 0) + (mParentEntry != null ? 1 : 0);
288    }
289
290    /**
291     * Returns the pre-entry corresponding to the index. If there is a parent and a top entry
292     * set, parent has an index of zero and top entry has an index of 1. Returns null if the
293     * specified index doesn't exist or is null.
294     * @param index should not be more than {@link #getPreEntryCount()} - 1
295     */
296    private BackStackEntry getPreEntry(int index) {
297        // If there's a parent entry, then return that for zero'th item, else top entry.
298        if (mParentEntry != null) {
299            return index == 0 ? mParentEntry : mTopEntry;
300        } else {
301            return mTopEntry;
302        }
303    }
304
305    void updateCrumbs() {
306        FragmentManager fm = mActivity.getFragmentManager();
307        int numEntries = fm.getBackStackEntryCount();
308        int numPreEntries = getPreEntryCount();
309        int numViews = mContainer.getChildCount();
310        for (int i = 0; i < numEntries + numPreEntries; i++) {
311            BackStackEntry bse = i < numPreEntries
312                    ? getPreEntry(i)
313                    : fm.getBackStackEntryAt(i - numPreEntries);
314            if (i < numViews) {
315                View v = mContainer.getChildAt(i);
316                Object tag = v.getTag();
317                if (tag != bse) {
318                    for (int j = i; j < numViews; j++) {
319                        mContainer.removeViewAt(i);
320                    }
321                    numViews = i;
322                }
323            }
324            if (i >= numViews) {
325                final View item = mInflater.inflate(mLayoutResId, this, false);
326                final TextView text = (TextView) item.findViewById(com.android.internal.R.id.title);
327                text.setText(bse.getBreadCrumbTitle());
328                text.setTag(bse);
329                text.setTextColor(mTextColor);
330                if (i == 0) {
331                    item.findViewById(com.android.internal.R.id.left_icon).setVisibility(View.GONE);
332                }
333                mContainer.addView(item);
334                text.setOnClickListener(mOnClickListener);
335            }
336        }
337        int viewI = numEntries + numPreEntries;
338        numViews = mContainer.getChildCount();
339        while (numViews > viewI) {
340            mContainer.removeViewAt(numViews - 1);
341            numViews--;
342        }
343        // Adjust the visibility and availability of the bread crumbs and divider
344        for (int i = 0; i < numViews; i++) {
345            final View child = mContainer.getChildAt(i);
346            // Disable the last one
347            child.findViewById(com.android.internal.R.id.title).setEnabled(i < numViews - 1);
348            if (mMaxVisible > 0) {
349                // Make only the last mMaxVisible crumbs visible
350                child.setVisibility(i < numViews - mMaxVisible ? View.GONE : View.VISIBLE);
351                final View leftIcon = child.findViewById(com.android.internal.R.id.left_icon);
352                // Remove the divider for all but the last mMaxVisible - 1
353                leftIcon.setVisibility(i > numViews - mMaxVisible && i != 0 ? View.VISIBLE
354                        : View.GONE);
355            }
356        }
357    }
358
359    private OnClickListener mOnClickListener = new OnClickListener() {
360        public void onClick(View v) {
361            if (v.getTag() instanceof BackStackEntry) {
362                BackStackEntry bse = (BackStackEntry) v.getTag();
363                if (bse == mParentEntry) {
364                    if (mParentClickListener != null) {
365                        mParentClickListener.onClick(v);
366                    }
367                } else {
368                    if (mOnBreadCrumbClickListener != null) {
369                        if (mOnBreadCrumbClickListener.onBreadCrumbClick(
370                                bse == mTopEntry ? null : bse, 0)) {
371                            return;
372                        }
373                    }
374                    if (bse == mTopEntry) {
375                        // Pop everything off the back stack.
376                        mActivity.getFragmentManager().popBackStack();
377                    } else {
378                        mActivity.getFragmentManager().popBackStack(bse.getId(), 0);
379                    }
380                }
381            }
382        }
383    };
384}
385