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 
17 package com.android.browser;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.drawable.Drawable;
22 import android.text.TextUtils;
23 import android.util.AttributeSet;
24 import android.util.TypedValue;
25 import android.view.Gravity;
26 import android.view.View;
27 import android.view.View.OnClickListener;
28 import android.widget.ImageButton;
29 import android.widget.ImageView;
30 import android.widget.LinearLayout;
31 import android.widget.TextView;
32 
33 import java.util.ArrayList;
34 import 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  */
41 public 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 {
onTop(BreadCrumbView view, int level, Object data)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      */
BreadCrumbView(Context context, AttributeSet attrs, int defStyle)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      */
BreadCrumbView(Context context, AttributeSet attrs)73     public BreadCrumbView(Context context, AttributeSet attrs) {
74         super(context, attrs);
75         init(context);
76     }
77 
78     /**
79      * @param context
80      */
BreadCrumbView(Context context)81     public BreadCrumbView(Context context) {
82         super(context);
83         init(context);
84     }
85 
init(Context ctx)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 
setUseBackButton(boolean useflag)100     public void setUseBackButton(boolean useflag) {
101         mUseBackButton = useflag;
102         updateVisible();
103     }
104 
setController(Controller ctl)105     public void setController(Controller ctl) {
106         mController = ctl;
107     }
108 
getMaxVisible()109     public int getMaxVisible() {
110         return mMaxVisible;
111     }
112 
setMaxVisible(int max)113     public void setMaxVisible(int max) {
114         mMaxVisible = max;
115         updateVisible();
116     }
117 
getTopLevel()118     public int getTopLevel() {
119         return mCrumbs.size();
120     }
121 
getTopData()122     public Object getTopData() {
123         Crumb c = getTopCrumb();
124         if (c != null) {
125             return c.data;
126         }
127         return null;
128     }
129 
size()130     public int size() {
131         return mCrumbs.size();
132     }
133 
clear()134     public void clear() {
135         while (mCrumbs.size() > 1) {
136             pop(false);
137         }
138         pop(true);
139     }
140 
notifyController()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 
pushView(String name, Object data)151     public View pushView(String name, Object data) {
152         return pushView(name, true, data);
153     }
154 
pushView(String name, boolean canGoBack, Object data)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 
pushView(View view, Object data)161     public void pushView(View view, Object data) {
162         Crumb crumb = new Crumb(view, true, data);
163         pushCrumb(crumb);
164     }
165 
popView()166     public void popView() {
167         pop(true);
168     }
169 
addBackButton()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 
pushCrumb(Crumb crumb)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 
addSeparator()197     private void addSeparator() {
198         View sep = makeDividerView();
199         sep.setLayoutParams(makeDividerLayoutParams());
200         addView(sep);
201     }
202 
makeDividerView()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 
makeDividerLayoutParams()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 
pop(boolean notify)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 
updateVisible()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 
removeLastView()283     private void removeLastView() {
284         int ix = getChildCount();
285         if (ix > 0) {
286             removeViewAt(ix-1);
287         }
288     }
289 
getTopCrumb()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
onClick(View v)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
getBaseline()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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 
Crumb(String title, boolean backEnabled, Object tag)351         public Crumb(String title, boolean backEnabled, Object tag) {
352             init(makeCrumbView(title), backEnabled, tag);
353         }
354 
Crumb(View view, boolean backEnabled, Object tag)355         public Crumb(View view, boolean backEnabled, Object tag) {
356             init(view, backEnabled, tag);
357         }
358 
init(View view, boolean back, Object tag)359         private void init(View view, boolean back, Object tag) {
360             canGoBack = back;
361             crumbView = view;
362             data = tag;
363         }
364 
makeCrumbView(String name)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