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