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