BreadCrumbView.java revision 71efc2bbf08574425a387c992e24cb9eaf0a6e6c
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 com.android.browser;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.drawable.Drawable;
22import android.text.TextUtils;
23import android.util.AttributeSet;
24import android.util.TypedValue;
25import android.view.Gravity;
26import android.view.View;
27import android.view.View.OnClickListener;
28import android.widget.ImageButton;
29import android.widget.ImageView;
30import android.widget.LinearLayout;
31import android.widget.TextView;
32
33import java.util.ArrayList;
34import java.util.List;
35
36/**
37 * Simple bread crumb view
38 * Use setController to receive callbacks from user interactions
39 * Use pushView, popView, clear, and getTopData to change/access the view stack
40 */
41public class BreadCrumbView extends LinearLayout implements OnClickListener {
42    private static final int DIVIDER_PADDING = 12; // dips
43
44    public interface Controller {
45        public void onTop(BreadCrumbView view, int level, Object data);
46    }
47
48    private ImageButton mBackButton;
49    private Controller mController;
50    private List<Crumb> mCrumbs;
51    private boolean mUseBackButton;
52    private Drawable mSeparatorDrawable;
53    private float mDividerPadding;
54    private int mMaxVisible = -1;
55    private Context mContext;
56
57    /**
58     * @param context
59     * @param attrs
60     * @param defStyle
61     */
62    public BreadCrumbView(Context context, AttributeSet attrs, int defStyle) {
63        super(context, attrs, defStyle);
64        init(context);
65    }
66
67    /**
68     * @param context
69     * @param attrs
70     */
71    public BreadCrumbView(Context context, AttributeSet attrs) {
72        super(context, attrs);
73        init(context);
74    }
75
76    /**
77     * @param context
78     */
79    public BreadCrumbView(Context context) {
80        super(context);
81        init(context);
82    }
83
84    private void init(Context ctx) {
85        mContext = ctx;
86        setFocusable(true);
87        mUseBackButton = false;
88        mCrumbs = new ArrayList<Crumb>();
89        TypedArray a = mContext.obtainStyledAttributes(com.android.internal.R.styleable.Theme);
90        mSeparatorDrawable = a.getDrawable(com.android.internal.R.styleable.Theme_dividerVertical);
91        a.recycle();
92        mDividerPadding = DIVIDER_PADDING * mContext.getResources().getDisplayMetrics().density;
93        addBackButton();
94    }
95
96    public void setUseBackButton(boolean useflag) {
97        mUseBackButton = useflag;
98        updateVisible();
99    }
100
101    public void setController(Controller ctl) {
102        mController = ctl;
103    }
104
105    public int getMaxVisible() {
106        return mMaxVisible;
107    }
108
109    public void setMaxVisible(int max) {
110        mMaxVisible = max;
111        updateVisible();
112    }
113
114    public int getTopLevel() {
115        return mCrumbs.size();
116    }
117
118    public Object getTopData() {
119        Crumb c = getTopCrumb();
120        if (c != null) {
121            return c.data;
122        }
123        return null;
124    }
125
126    public int size() {
127        return mCrumbs.size();
128    }
129
130    public void clear() {
131        while (mCrumbs.size() > 1) {
132            pop(false);
133        }
134        pop(true);
135    }
136
137    public void notifyController() {
138        if (mController != null) {
139            if (mCrumbs.size() > 0) {
140                mController.onTop(this, mCrumbs.size(), getTopCrumb().data);
141            } else {
142                mController.onTop(this, 0, null);
143            }
144        }
145    }
146
147    public View pushView(String name, Object data) {
148        return pushView(name, true, data);
149    }
150
151    public View pushView(String name, boolean canGoBack, Object data) {
152        Crumb crumb = new Crumb(name, canGoBack, data);
153        pushCrumb(crumb);
154        return crumb.crumbView;
155    }
156
157    public void pushView(View view, Object data) {
158        Crumb crumb = new Crumb(view, true, data);
159        pushCrumb(crumb);
160    }
161
162    public void popView() {
163        pop(true);
164    }
165
166    private void addBackButton() {
167        mBackButton = new ImageButton(mContext);
168        mBackButton.setImageResource(R.drawable.ic_back_hierarchy_holo_dark);
169        TypedValue outValue = new TypedValue();
170        getContext().getTheme().resolveAttribute(
171                android.R.attr.selectableItemBackground, outValue, true);
172        int resid = outValue.resourceId;
173        mBackButton.setBackgroundResource(resid);
174        mBackButton.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
175                LayoutParams.MATCH_PARENT));
176        mBackButton.setOnClickListener(this);
177        mBackButton.setVisibility(View.GONE);
178        addView(mBackButton, 0);
179    }
180
181    private void pushCrumb(Crumb crumb) {
182        if (mCrumbs.size() > 0) {
183            addSeparator();
184        }
185        mCrumbs.add(crumb);
186        addView(crumb.crumbView);
187        updateVisible();
188        crumb.crumbView.setOnClickListener(this);
189    }
190
191    private void addSeparator() {
192        View sep = makeDividerView();
193        sep.setLayoutParams(makeDividerLayoutParams());
194        addView(sep);
195    }
196
197    private ImageView makeDividerView() {
198        ImageView result = new ImageView(mContext);
199        result.setImageDrawable(mSeparatorDrawable);
200        result.setScaleType(ImageView.ScaleType.FIT_XY);
201        return result;
202    }
203
204    private LayoutParams makeDividerLayoutParams() {
205        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
206                LayoutParams.MATCH_PARENT);
207        params.topMargin = (int) mDividerPadding;
208        params.bottomMargin = (int) mDividerPadding;
209        return params;
210    }
211
212    private void pop(boolean notify) {
213        int n = mCrumbs.size();
214        if (n > 0) {
215            removeLastView();
216            if (!mUseBackButton || (n > 1)) {
217                // remove separator
218                removeLastView();
219            }
220            mCrumbs.remove(n - 1);
221            if (mUseBackButton) {
222                Crumb top = getTopCrumb();
223                if (top != null && top.canGoBack) {
224                    mBackButton.setVisibility(View.VISIBLE);
225                } else {
226                    mBackButton.setVisibility(View.GONE);
227                }
228            }
229            updateVisible();
230            if (notify) {
231                notifyController();
232            }
233        }
234    }
235
236    private void updateVisible() {
237        // start at index 1 (0 == back button)
238        int childIndex = 1;
239        if (mMaxVisible >= 0) {
240            int invisibleCrumbs = size() - mMaxVisible;
241            if (invisibleCrumbs > 0) {
242                int crumbIndex = 0;
243                while (crumbIndex < invisibleCrumbs) {
244                    // Set the crumb to GONE.
245                    getChildAt(childIndex).setVisibility(View.GONE);
246                    childIndex++;
247                    // Each crumb is followed by a separator (except the last
248                    // one).  Also make it GONE
249                    if (getChildAt(childIndex) != null) {
250                        getChildAt(childIndex).setVisibility(View.GONE);
251                    }
252                    childIndex++;
253                    // Move to the next crumb.
254                    crumbIndex++;
255                }
256            }
257            // Make sure the last two are visible.
258            int childCount = getChildCount();
259            while (childIndex < childCount) {
260                getChildAt(childIndex).setVisibility(View.VISIBLE);
261                childIndex++;
262            }
263        } else {
264            int count = getChildCount();
265            for (int i = childIndex; i < count ; i++) {
266                getChildAt(i).setVisibility(View.VISIBLE);
267            }
268        }
269        if (mUseBackButton) {
270            boolean canGoBack = getTopCrumb() != null ? getTopCrumb().canGoBack : false;
271            mBackButton.setVisibility(canGoBack ? View.VISIBLE : View.GONE);
272        } else {
273            mBackButton.setVisibility(View.GONE);
274        }
275    }
276
277    private void removeLastView() {
278        int ix = getChildCount();
279        if (ix > 0) {
280            removeViewAt(ix-1);
281        }
282    }
283
284    Crumb getTopCrumb() {
285        Crumb crumb = null;
286        if (mCrumbs.size() > 0) {
287            crumb = mCrumbs.get(mCrumbs.size() - 1);
288        }
289        return crumb;
290    }
291
292    @Override
293    public void onClick(View v) {
294        if (mBackButton == v) {
295            popView();
296            notifyController();
297        } else {
298            // pop until view matches crumb view
299            while (v != getTopCrumb().crumbView) {
300                pop(false);
301            }
302            notifyController();
303        }
304    }
305    @Override
306    public int getBaseline() {
307        int ix = getChildCount();
308        if (ix > 0) {
309            // If there is at least one crumb, the baseline will be its
310            // baseline.
311            return getChildAt(ix-1).getBaseline();
312        }
313        return super.getBaseline();
314    }
315
316    @Override
317    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
318        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
319        int height = mSeparatorDrawable.getIntrinsicHeight();
320        if (getMeasuredHeight() < height) {
321            // This should only be an issue if there are currently no separators
322            // showing; i.e. if there is one crumb and no back button.
323            int mode = View.MeasureSpec.getMode(heightMeasureSpec);
324            switch(mode) {
325                case View.MeasureSpec.AT_MOST:
326                    if (View.MeasureSpec.getSize(heightMeasureSpec) < height) {
327                        return;
328                    }
329                    break;
330                case View.MeasureSpec.EXACTLY:
331                    return;
332                default:
333                    break;
334            }
335            setMeasuredDimension(getMeasuredWidth(), height);
336        }
337    }
338
339    class Crumb {
340
341        public View crumbView;
342        public boolean canGoBack;
343        public Object data;
344
345        public Crumb(String title, boolean backEnabled, Object tag) {
346            init(makeCrumbView(title), backEnabled, tag);
347        }
348
349        public Crumb(View view, boolean backEnabled, Object tag) {
350            init(view, backEnabled, tag);
351        }
352
353        private void init(View view, boolean back, Object tag) {
354            canGoBack = back;
355            crumbView = view;
356            data = tag;
357        }
358
359        private TextView makeCrumbView(String name) {
360            TextView tv = new TextView(mContext);
361            tv.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
362            tv.setPadding(16, 0, 16, 0);
363            tv.setGravity(Gravity.CENTER_VERTICAL);
364            tv.setText(name);
365            tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
366                    LayoutParams.MATCH_PARENT));
367            tv.setMaxWidth(mContext.getResources().getInteger(
368                    R.integer.max_width_crumb));
369            tv.setMaxLines(1);
370            tv.setEllipsize(TextUtils.TruncateAt.END);
371            return tv;
372        }
373
374    }
375
376}
377