TabBar.java revision 5d43ce897758b7cb5deadc1203717b965334784c
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 com.android.browser.BrowserWebView.ScrollListener; 20 21import android.animation.Animator; 22import android.animation.Animator.AnimatorListener; 23import android.animation.AnimatorSet; 24import android.animation.ObjectAnimator; 25import android.app.Activity; 26import android.content.Context; 27import android.content.res.Configuration; 28import android.content.res.Resources; 29import android.graphics.Bitmap; 30import android.graphics.BitmapShader; 31import android.graphics.Canvas; 32import android.graphics.Matrix; 33import android.graphics.Paint; 34import android.graphics.Path; 35import android.graphics.Shader; 36import android.graphics.drawable.BitmapDrawable; 37import android.graphics.drawable.Drawable; 38import android.graphics.drawable.LayerDrawable; 39import android.graphics.drawable.PaintDrawable; 40import android.view.ContextMenu; 41import android.view.Gravity; 42import android.view.LayoutInflater; 43import android.view.MenuInflater; 44import android.view.View; 45import android.view.View.OnClickListener; 46import android.webkit.WebView; 47import android.widget.ImageButton; 48import android.widget.ImageView; 49import android.widget.LinearLayout; 50import android.widget.TextView; 51 52import java.util.HashMap; 53import java.util.List; 54import java.util.Map; 55 56/** 57 * tabbed title bar for xlarge screen browser 58 */ 59public class TabBar extends LinearLayout 60 implements ScrollListener, OnClickListener { 61 62 private static final int PROGRESS_MAX = 100; 63 64 private Activity mActivity; 65 private UiController mUiController; 66 private TabControl mTabControl; 67 private XLargeUi mUi; 68 69 private int mTabWidthSelected; 70 private int mTabWidthUnselected; 71 72 private TabScrollView mTabs; 73 74 private ImageButton mNewTab; 75 private int mButtonWidth; 76 77 private Map<Tab, TabView> mTabMap; 78 79 private Drawable mGenericFavicon; 80 81 private int mCurrentTextureWidth = 0; 82 private int mCurrentTextureHeight = 0; 83 84 private Drawable mActiveDrawable; 85 private Drawable mInactiveDrawable; 86 87 private final Paint mActiveShaderPaint = new Paint(); 88 private final Paint mInactiveShaderPaint = new Paint(); 89 private final Paint mFocusPaint = new Paint(); 90 private final Matrix mActiveMatrix = new Matrix(); 91 private final Matrix mInactiveMatrix = new Matrix(); 92 93 private BitmapShader mActiveShader; 94 private BitmapShader mInactiveShader; 95 96 private int mTabOverlap; 97 private int mAddTabOverlap; 98 private int mTabSliceWidth; 99 private boolean mUseQuickControls; 100 101 public TabBar(Activity activity, UiController controller, XLargeUi ui) { 102 super(activity); 103 mActivity = activity; 104 mUiController = controller; 105 mTabControl = mUiController.getTabControl(); 106 mUi = ui; 107 Resources res = activity.getResources(); 108 mTabWidthSelected = (int) res.getDimension(R.dimen.tab_width_selected); 109 mTabWidthUnselected = (int) res.getDimension(R.dimen.tab_width_unselected); 110 mActiveDrawable = res.getDrawable(R.drawable.bg_urlbar); 111 mInactiveDrawable = res.getDrawable(R.drawable.browsertab_inactive); 112 113 mTabMap = new HashMap<Tab, TabView>(); 114 LayoutInflater factory = LayoutInflater.from(activity); 115 factory.inflate(R.layout.tab_bar, this); 116 setPadding(0, (int) res.getDimension(R.dimen.tab_padding_top), 0, 0); 117 mTabs = (TabScrollView) findViewById(R.id.tabs); 118 mNewTab = (ImageButton) findViewById(R.id.newtab); 119 mNewTab.setOnClickListener(this); 120 mGenericFavicon = res.getDrawable(R.drawable.app_web_browser_sm); 121 122 updateTabs(mUiController.getTabs()); 123 mButtonWidth = -1; 124 // tab dimensions 125 mTabOverlap = (int) res.getDimension(R.dimen.tab_overlap); 126 mAddTabOverlap = (int) res.getDimension(R.dimen.tab_addoverlap); 127 mTabSliceWidth = (int) res.getDimension(R.dimen.tab_slice); 128 129 mActiveShaderPaint.setStyle(Paint.Style.FILL); 130 mActiveShaderPaint.setAntiAlias(true); 131 132 mInactiveShaderPaint.setStyle(Paint.Style.FILL); 133 mInactiveShaderPaint.setAntiAlias(true); 134 135 mFocusPaint.setStyle(Paint.Style.STROKE); 136 mFocusPaint.setStrokeWidth(res.getDimension(R.dimen.tab_focus_stroke)); 137 mFocusPaint.setAntiAlias(true); 138 mFocusPaint.setColor(res.getColor(R.color.tabFocusHighlight)); 139 } 140 141 @Override 142 public void onConfigurationChanged(Configuration config) { 143 super.onConfigurationChanged(config); 144 Resources res = mActivity.getResources(); 145 mTabWidthSelected = (int) res.getDimension(R.dimen.tab_width_selected); 146 mTabWidthUnselected = (int) res.getDimension(R.dimen.tab_width_unselected); 147 // force update of tab bar 148 mTabs.updateLayout(); 149 } 150 151 void setUseQuickControls(boolean useQuickControls) { 152 mUseQuickControls = useQuickControls; 153 mNewTab.setVisibility(mUseQuickControls ? View.GONE 154 : View.VISIBLE); 155 } 156 157 int getTabCount() { 158 return mTabMap.size(); 159 } 160 161 void updateTabs(List<Tab> tabs) { 162 mTabs.clearTabs(); 163 mTabMap.clear(); 164 for (Tab tab : tabs) { 165 TabView tv = buildTabView(tab); 166 mTabs.addTab(tv); 167 } 168 mTabs.setSelectedTab(mTabControl.getCurrentPosition()); 169 } 170 171 @Override 172 protected void onMeasure(int hspec, int vspec) { 173 super.onMeasure(hspec, vspec); 174 int w = getMeasuredWidth(); 175 // adjust for new tab overlap 176 if (!mUseQuickControls) { 177 w -= mAddTabOverlap; 178 } 179 setMeasuredDimension(w, getMeasuredHeight()); 180 } 181 182 @Override 183 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 184 // use paddingLeft and paddingTop 185 int pl = getPaddingLeft(); 186 int pt = getPaddingTop(); 187 int sw = mTabs.getMeasuredWidth(); 188 int w = right - left - pl; 189 if (mUseQuickControls) { 190 mButtonWidth = 0; 191 } else { 192 mButtonWidth = mNewTab.getMeasuredWidth() - mAddTabOverlap; 193 if (w-sw < mButtonWidth) { 194 sw = w - mButtonWidth; 195 } 196 } 197 mTabs.layout(pl, pt, pl + sw, bottom - top); 198 // adjust for overlap 199 if (!mUseQuickControls) { 200 mNewTab.layout(pl + sw - mAddTabOverlap, pt, 201 pl + sw + mButtonWidth - mAddTabOverlap, bottom - top); 202 } 203 } 204 205 public void onClick(View view) { 206 if (mNewTab == view) { 207 mUiController.openTabToHomePage(); 208 } else if (mTabs.getSelectedTab() == view) { 209 if (mUseQuickControls) { 210 if (mUi.isTitleBarShowing() && !isLoading()) { 211 mUi.stopEditingUrl(); 212 mUi.hideTitleBar(); 213 } else { 214 mUi.stopWebViewScrolling(); 215 mUi.editUrl(false); 216 } 217 } else if (mUi.isTitleBarShowing() && !isLoading()) { 218 mUi.stopEditingUrl(); 219 mUi.hideTitleBar(); 220 } else { 221 showUrlBar(); 222 } 223 } else if (view instanceof TabView) { 224 final Tab tab = ((TabView) view).mTab; 225 int ix = mTabs.getChildIndex(view); 226 if (ix >= 0) { 227 mTabs.setSelectedTab(ix); 228 mUiController.switchToTab(tab); 229 } 230 } 231 } 232 233 private void showUrlBar() { 234 mUi.stopWebViewScrolling(); 235 mUi.showTitleBar(); 236 } 237 238 void showTitleBarIndicator(boolean show) { 239 Tab tab = mTabControl.getCurrentTab(); 240 if (tab != null && !tab.isSnapshot()) { 241 TabView tv = mTabMap.get(tab); 242 if (tv != null) { 243 tv.showIndicator(show); 244 } 245 } 246 } 247 248 boolean showsTitleBarIndicator() { 249 Tab tab = mTabControl.getCurrentTab(); 250 if (tab != null) { 251 TabView tv = mTabMap.get(tab); 252 if (tv != null) { 253 return tv.showsIndicator(); 254 } 255 } 256 return false; 257 } 258 259 // callback after fake titlebar is shown 260 void onShowTitleBar() { 261 showTitleBarIndicator(false); 262 } 263 264 // callback after fake titlebar is hidden 265 void onHideTitleBar() { 266 Tab tab = mTabControl.getCurrentTab(); 267 WebView w = tab.getWebView(); 268 if (w != null) { 269 showTitleBarIndicator(w.getVisibleTitleHeight() == 0); 270 } 271 } 272 273 // webview scroll listener 274 275 @Override 276 public void onScroll(int visibleTitleHeight, boolean userInitiated) { 277 if (mUseQuickControls) return; 278 // isLoading is using the current tab, which initially might not be set yet 279 if (mTabControl.getCurrentTab() != null 280 && !isLoading()) { 281 if (visibleTitleHeight == 0) { 282 if (!showsTitleBarIndicator() 283 && (!mUi.isEditingUrl() || userInitiated)) { 284 mUi.hideTitleBar(); 285 showTitleBarIndicator(true); 286 } 287 } else { 288 if (showsTitleBarIndicator()) { 289 showTitleBarIndicator(false); 290 } 291 } 292 } 293 } 294 295 @Override 296 public void createContextMenu(ContextMenu menu) { 297 MenuInflater inflater = mActivity.getMenuInflater(); 298 inflater.inflate(R.menu.title_context, menu); 299 mActivity.onCreateContextMenu(menu, this, null); 300 } 301 302 private TabView buildTabView(Tab tab) { 303 TabView tabview = new TabView(mActivity, tab); 304 mTabMap.put(tab, tabview); 305 tabview.setOnClickListener(this); 306 return tabview; 307 } 308 309 private static Bitmap getDrawableAsBitmap(Drawable drawable, int width, int height) { 310 Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 311 Canvas c = new Canvas(b); 312 drawable.setBounds(0, 0, width, height); 313 drawable.draw(c); 314 return b; 315 } 316 317 /** 318 * View used in the tab bar 319 */ 320 class TabView extends LinearLayout implements OnClickListener { 321 322 Tab mTab; 323 View mTabContent; 324 TextView mTitle; 325 View mIndicator; 326 View mIncognito; 327 View mSnapshot; 328 ImageView mIconView; 329 ImageView mLock; 330 ImageView mClose; 331 boolean mSelected; 332 boolean mInLoad; 333 Path mPath; 334 Path mFocusPath; 335 int[] mWindowPos; 336 337 /** 338 * @param context 339 */ 340 public TabView(Context context, Tab tab) { 341 super(context); 342 setWillNotDraw(false); 343 mPath = new Path(); 344 mFocusPath = new Path(); 345 mWindowPos = new int[2]; 346 mTab = tab; 347 setGravity(Gravity.CENTER_VERTICAL); 348 setOrientation(LinearLayout.HORIZONTAL); 349 setPadding(mTabOverlap, 0, mTabSliceWidth, 0); 350 LayoutInflater inflater = LayoutInflater.from(getContext()); 351 mTabContent = inflater.inflate(R.layout.tab_title, this, true); 352 mTitle = (TextView) mTabContent.findViewById(R.id.title); 353 mIconView = (ImageView) mTabContent.findViewById(R.id.favicon); 354 mLock = (ImageView) mTabContent.findViewById(R.id.lock); 355 mClose = (ImageView) mTabContent.findViewById(R.id.close); 356 mClose.setOnClickListener(this); 357 mIncognito = mTabContent.findViewById(R.id.incognito); 358 mSnapshot = mTabContent.findViewById(R.id.snapshot); 359 mIndicator = mTabContent.findViewById(R.id.chevron); 360 mSelected = false; 361 mInLoad = false; 362 // update the status 363 updateFromTab(); 364 } 365 366 void showIndicator(boolean show) { 367 if (mSelected) { 368 mIndicator.setVisibility(show ? View.VISIBLE : View.GONE); 369 LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams(); 370 if (show) { 371 lp.width = mTabWidthSelected + mIndicator.getWidth(); 372 } else { 373 lp.width = mTabWidthSelected; 374 } 375 lp.height = LayoutParams.MATCH_PARENT; 376 setLayoutParams(lp); 377 } else { 378 mIndicator.setVisibility(View.GONE); 379 } 380 } 381 382 boolean showsIndicator() { 383 return (mIndicator.getVisibility() == View.VISIBLE); 384 } 385 386 @Override 387 public void onClick(View v) { 388 if (v == mClose) { 389 closeTab(); 390 } 391 } 392 393 private void updateFromTab() { 394 String displayTitle = mTab.getTitle(); 395 if (displayTitle == null) { 396 displayTitle = mTab.getUrl(); 397 } 398 setDisplayTitle(displayTitle); 399 setProgress(mTab.getLoadProgress()); 400 if (mTab.getFavicon() != null) { 401 setFavicon(renderFavicon(mTab.getFavicon())); 402 } 403 updateTabIcons(); 404 } 405 406 private void updateTabIcons() { 407 mIncognito.setVisibility( 408 mTab.isPrivateBrowsingEnabled() ? 409 View.VISIBLE : View.GONE); 410 mSnapshot.setVisibility(mTab.isSnapshot() 411 ? View.VISIBLE : View.GONE); 412 } 413 414 @Override 415 public void setActivated(boolean selected) { 416 mSelected = selected; 417 mClose.setVisibility(mSelected ? View.VISIBLE : View.GONE); 418 mIndicator.setVisibility(View.GONE); 419 mTitle.setTextAppearance(mActivity, mSelected ? 420 R.style.TabTitleSelected : R.style.TabTitleUnselected); 421 setHorizontalFadingEdgeEnabled(!mSelected); 422 super.setActivated(selected); 423 updateLayoutParams(); 424 setFocusable(!selected); 425 postInvalidate(); 426 } 427 428 public void updateLayoutParams() { 429 LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams(); 430 lp.width = mSelected ? mTabWidthSelected : mTabWidthUnselected; 431 lp.height = LayoutParams.MATCH_PARENT; 432 setLayoutParams(lp); 433 } 434 435 void setDisplayTitle(String title) { 436 mTitle.setText(title); 437 } 438 439 void setFavicon(Drawable d) { 440 mIconView.setImageDrawable(d); 441 } 442 443 void setLock(Drawable d) { 444 if (null == d) { 445 mLock.setVisibility(View.GONE); 446 } else { 447 mLock.setImageDrawable(d); 448 mLock.setVisibility(View.VISIBLE); 449 } 450 } 451 452 void setProgress(int newProgress) { 453 if (newProgress >= PROGRESS_MAX) { 454 mInLoad = false; 455 } else { 456 if (!mInLoad && getWindowToken() != null) { 457 mInLoad = true; 458 } 459 } 460 } 461 462 private void closeTab() { 463 if (mTab == mTabControl.getCurrentTab()) { 464 mUiController.closeCurrentTab(); 465 } else { 466 mUiController.closeTab(mTab); 467 } 468 } 469 470 @Override 471 protected void onLayout(boolean changed, int l, int t, int r, int b) { 472 super.onLayout(changed, l, t, r, b); 473 setTabPath(mPath, 0, 0, r - l, b - t); 474 setFocusPath(mFocusPath, 0, 0, r - l, b - t); 475 } 476 477 @Override 478 protected void dispatchDraw(Canvas canvas) { 479 if (mCurrentTextureWidth != mUi.getContentWidth() || 480 mCurrentTextureHeight != getHeight()) { 481 mCurrentTextureWidth = mUi.getContentWidth(); 482 mCurrentTextureHeight = getHeight(); 483 484 if (mCurrentTextureWidth > 0 && mCurrentTextureHeight > 0) { 485 Bitmap activeTexture = getDrawableAsBitmap(mActiveDrawable, 486 mCurrentTextureWidth, mCurrentTextureHeight); 487 Bitmap inactiveTexture = getDrawableAsBitmap(mInactiveDrawable, 488 mCurrentTextureWidth, mCurrentTextureHeight); 489 490 mActiveShader = new BitmapShader(activeTexture, 491 Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 492 mActiveShaderPaint.setShader(mActiveShader); 493 494 mInactiveShader = new BitmapShader(inactiveTexture, 495 Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 496 mInactiveShaderPaint.setShader(mInactiveShader); 497 } 498 } 499 // add some monkey protection 500 if ((mActiveShader != null) && (mInactiveShader != null)) { 501 int state = canvas.save(); 502 getLocationInWindow(mWindowPos); 503 Paint paint = mSelected ? mActiveShaderPaint : mInactiveShaderPaint; 504 drawClipped(canvas, paint, mPath, mWindowPos[0]); 505 canvas.restoreToCount(state); 506 } 507 super.dispatchDraw(canvas); 508 } 509 510 private void drawClipped(Canvas canvas, Paint paint, Path clipPath, int left) { 511 // TODO: We should change the matrix/shader only when needed 512 final Matrix matrix = mSelected ? mActiveMatrix : mInactiveMatrix; 513 matrix.setTranslate(-left, 0.0f); 514 (mSelected ? mActiveShader : mInactiveShader).setLocalMatrix(matrix); 515 canvas.drawPath(clipPath, paint); 516 if (isFocused()) { 517 canvas.drawPath(mFocusPath, mFocusPaint); 518 } 519 } 520 521 private void setTabPath(Path path, int l, int t, int r, int b) { 522 path.reset(); 523 path.moveTo(l, b); 524 path.lineTo(l, t); 525 path.lineTo(r - mTabSliceWidth, t); 526 path.lineTo(r, b); 527 path.close(); 528 } 529 530 private void setFocusPath(Path path, int l, int t, int r, int b) { 531 path.reset(); 532 path.moveTo(l, b); 533 path.lineTo(l, t); 534 path.lineTo(r - mTabSliceWidth, t); 535 path.lineTo(r, b); 536 } 537 538 } 539 540 static Drawable createFaviconBackground(Context context) { 541 PaintDrawable faviconBackground = new PaintDrawable(); 542 Resources res = context.getResources(); 543 faviconBackground.getPaint().setColor(context.getResources() 544 .getColor(R.color.tabFaviconBackground)); 545 faviconBackground.setCornerRadius( 546 res.getDimension(R.dimen.tab_favicon_corner_radius)); 547 return faviconBackground; 548 } 549 550 private Drawable renderFavicon(Bitmap icon) { 551 Drawable[] array = new Drawable[2]; 552 array[0] = createFaviconBackground(getContext()); 553 if (icon == null) { 554 array[1] = mGenericFavicon; 555 } else { 556 array[1] = new BitmapDrawable(icon); 557 } 558 LayerDrawable d = new LayerDrawable(array); 559 d.setLayerInset(1, 2, 2, 2, 2); 560 return d; 561 } 562 563 private void animateTabOut(final Tab tab, final TabView tv) { 564 ObjectAnimator scalex = ObjectAnimator.ofFloat(tv, "scaleX", 1.0f, 0.0f); 565 ObjectAnimator scaley = ObjectAnimator.ofFloat(tv, "scaleY", 1.0f, 0.0f); 566 ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "alpha", 1.0f, 0.0f); 567 AnimatorSet animator = new AnimatorSet(); 568 animator.playTogether(scalex, scaley, alpha); 569 animator.setDuration(150); 570 animator.addListener(new AnimatorListener() { 571 572 @Override 573 public void onAnimationCancel(Animator animation) { 574 } 575 576 @Override 577 public void onAnimationEnd(Animator animation) { 578 mTabs.removeTab(tv); 579 mTabMap.remove(tab); 580 mUi.onRemoveTabCompleted(tab); 581 } 582 583 @Override 584 public void onAnimationRepeat(Animator animation) { 585 } 586 587 @Override 588 public void onAnimationStart(Animator animation) { 589 } 590 591 }); 592 animator.start(); 593 } 594 595 private void animateTabIn(final Tab tab, final TabView tv) { 596 ObjectAnimator scalex = ObjectAnimator.ofFloat(tv, "scaleX", 0.0f, 1.0f); 597 scalex.setDuration(150); 598 scalex.addListener(new AnimatorListener() { 599 600 @Override 601 public void onAnimationCancel(Animator animation) { 602 } 603 604 @Override 605 public void onAnimationEnd(Animator animation) { 606 mUi.onAddTabCompleted(tab); 607 } 608 609 @Override 610 public void onAnimationRepeat(Animator animation) { 611 } 612 613 @Override 614 public void onAnimationStart(Animator animation) { 615 mTabs.addTab(tv); 616 } 617 618 }); 619 scalex.start(); 620 } 621 622 // TabChangeListener implementation 623 624 public void onSetActiveTab(Tab tab) { 625 mTabs.setSelectedTab(mTabControl.getTabPosition(tab)); 626 TabView tv = mTabMap.get(tab); 627 if (tv != null) { 628 tv.setProgress(tv.mTab.getLoadProgress()); 629 // update the scroll state 630 WebView webview = tab.getWebView(); 631 if (webview != null) { 632 int h = webview.getVisibleTitleHeight(); 633 onScroll(h, true); 634 } 635 } 636 } 637 638 public void onFavicon(Tab tab, Bitmap favicon) { 639 TabView tv = mTabMap.get(tab); 640 if (tv != null) { 641 tv.setFavicon(renderFavicon(favicon)); 642 } 643 } 644 645 public void onNewTab(Tab tab) { 646 TabView tv = buildTabView(tab); 647 animateTabIn(tab, tv); 648 } 649 650 public void onProgress(Tab tab, int progress) { 651 TabView tv = mTabMap.get(tab); 652 if (tv != null) { 653 tv.setProgress(progress); 654 } 655 } 656 657 public void onRemoveTab(Tab tab) { 658 TabView tv = mTabMap.get(tab); 659 if (tv != null) { 660 animateTabOut(tab, tv); 661 } else { 662 mTabMap.remove(tab); 663 } 664 } 665 666 public void onUrlAndTitle(Tab tab, String url, String title) { 667 TabView tv = mTabMap.get(tab); 668 if (tv != null) { 669 if (title != null) { 670 tv.setDisplayTitle(title); 671 } else if (url != null) { 672 tv.setDisplayTitle(UrlUtils.stripUrl(url)); 673 } 674 tv.updateTabIcons(); 675 } 676 } 677 678 private boolean isLoading() { 679 TabView tv = mTabMap.get(mTabControl.getCurrentTab()); 680 if (tv != null) { 681 return tv.mInLoad; 682 } else { 683 return false; 684 } 685 } 686 687} 688