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