1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.compositor.layouts.phone.stack; 6 7 import android.animation.Animator; 8 import android.animation.AnimatorListenerAdapter; 9 import android.content.Context; 10 import android.content.res.Resources; 11 import android.graphics.RectF; 12 13 import androidx.annotation.IntDef; 14 import androidx.annotation.VisibleForTesting; 15 16 import org.chromium.base.MathUtils; 17 import org.chromium.base.metrics.RecordUserAction; 18 import org.chromium.chrome.R; 19 import org.chromium.chrome.browser.compositor.layouts.Layout; 20 import org.chromium.chrome.browser.compositor.layouts.Layout.Orientation; 21 import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab; 22 import org.chromium.chrome.browser.compositor.layouts.eventfilter.ScrollDirection; 23 import org.chromium.chrome.browser.compositor.layouts.phone.StackLayoutBase; 24 import org.chromium.chrome.browser.compositor.layouts.phone.stack.StackAnimation.OverviewAnimationType; 25 import org.chromium.chrome.browser.flags.CachedFeatureFlags; 26 import org.chromium.chrome.browser.flags.ChromeFeatureList; 27 import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler; 28 import org.chromium.chrome.browser.layouts.animation.FloatProperty; 29 import org.chromium.chrome.browser.tab.Tab; 30 import org.chromium.chrome.browser.tabmodel.TabList; 31 import org.chromium.chrome.browser.tabmodel.TabModelUtils; 32 import org.chromium.ui.base.LocalizationUtils; 33 34 import java.lang.annotation.Retention; 35 import java.lang.annotation.RetentionPolicy; 36 37 /** 38 * Handles all the drawing and events of a stack of stackTabs. 39 * 40 * @VisibleForTesting 41 */ 42 public abstract class Stack { 43 public static final int MAX_NUMBER_OF_STACKED_TABS_TOP = 3; 44 public static final int MAX_NUMBER_OF_STACKED_TABS_BOTTOM = 3; 45 46 private static final float STACK_PORTRAIT_Y_OFFSET_PROPORTION = -0.8f; 47 private static final float STACK_LANDSCAPE_START_OFFSET_PROPORTION = -0.7f; 48 private static final float STACK_LANDSCAPE_Y_OFFSET_PROPORTION = -0.5f; 49 50 @IntDef({DragLock.NONE, DragLock.SCROLL, DragLock.DISCARD}) 51 @Retention(RetentionPolicy.SOURCE) 52 public @interface DragLock { 53 int NONE = 0; 54 int SCROLL = 1; 55 int DISCARD = 2; 56 } 57 58 /** 59 * The percentage of the screen to cover for the discarded tab to be fully transparent. 60 */ 61 public static final float DISCARD_RANGE_SCREEN = 0.7f; 62 63 /** 64 * The percentage the tab need to be dragged to actually discard the card. 65 */ 66 private static final float DISCARD_COMMIT_THRESHOLD = 0.4f; 67 68 /** 69 * The percentage of the side of the tab that is inactive to swipe to discard. As this is 70 * a distance computed from both edges, meaningful value ranges in [0 ... 0.5]. 71 */ 72 private static final float DISCARD_SAFE_SELECTION_PCTG = 0.1f; 73 74 /** 75 * The minimum scale the tab can reach when being discarded by a click. 76 */ 77 private static final float DISCARD_END_SCALE_CLICK = 0.7f; 78 79 /** 80 * The minimum scale the tab can reach when being discarded by a swipe. 81 */ 82 private static final float DISCARD_END_SCALE_SWIPE = 0.5f; 83 84 /** 85 * The delta time applied on the velocity from the fling. This is to compute the kick to 86 * help discarding a card. 87 */ 88 private static final float DISCARD_FLING_DT = 1.0f / 45.0f; 89 90 /** 91 * The maximum contribution of the fling. This is in percentage of the range. 92 */ 93 private static final float DISCARD_FLING_MAX_CONTRIBUTION = 0.4f; 94 95 /** 96 * How much to scale the max overscroll angle when tabs are tilting backwards. 97 */ 98 private static final float BACKWARDS_TILT_SCALE = 0.5f; 99 100 /** 101 * When overscrolling towards the top or left of the screen, what portion of 102 * the overscroll should be devoted to sliding the tabs together. The rest 103 * of the overscroll is used for tilting. 104 */ 105 private static final float OVERSCROLL_TOP_SLIDE_PCTG = 0.25f; 106 107 /** 108 * Scale max under/over scroll by this amount when flinging. 109 */ 110 private static final float MAX_OVER_FLING_SCALE = 0.5f; 111 112 /** 113 * mMaxUnderScroll is determined by multing mMaxOverScroll with 114 * MAX_UNDER_SCROLL_SCALE 115 */ 116 private static final float MAX_UNDER_SCROLL_SCALE = 2.0f; 117 118 /** 119 * Drags that are mostly horizontal (within 30 degrees) signal that 120 * a user is discarding a tab. 121 */ 122 private static final float DRAG_ANGLE_THRESHOLD = (float) Math.tan(Math.toRadians(30.0)); 123 124 /** 125 * Reset the scroll mode after this number of milliseconds of inactivity or small motions. 126 */ 127 private static final long DRAG_TIME_THRESHOLD = 400; 128 129 /** 130 * Minimum motion threshold to lock the scroll mode. 131 */ 132 private static final float DRAG_MOTION_THRESHOLD_DP = 1.25f; 133 134 /** 135 * The number of attempt to get the full roll overscroll animation. 136 */ 137 private static final int OVERSCROLL_FULL_ROLL_TRIGGER = 5; 138 139 /** 140 * Percentage of the screen to wrap the scroll space. 141 */ 142 private static final float SCROLL_WARP_PCTG = 0.4f; 143 144 /** 145 * Percentage of the screen a swipe gesture must traverse before it is allowed to be 146 * canceled. 147 */ 148 private static final float SWIPE_LANDSCAPE_THRESHOLD = 0.19f; 149 150 /** 151 * How far to place the tab to the left of the user's finger when swiping in dp. This keeps 152 * the tab under the user's finger. 153 */ 154 private static final float LANDSCAPE_SWIPE_DRAG_TAB_OFFSET_DP = 40.f; 155 156 // TODO(dtrainor): Investigate removing this. 157 private static final float BORDER_THICKNESS_DP = 4.f; 158 159 // External References 160 protected TabList mTabList; 161 162 // True when the stack is still visible for animation but it is going to be empty. 163 private boolean mIsDying; 164 165 // Screen State Variables 166 protected int mSpacing; 167 protected StackTab[] mStackTabs; // mStackTabs can be null if there are no tabs 168 169 // Overscroll 170 protected StackScroller mScroller; 171 private float mOverScrollOffset; 172 private int mOverScrollDerivative; 173 private int mOverScrollCounter; 174 private float mMaxOverScroll; // This will be updated from dimens.xml 175 protected float mMaxUnderScroll; 176 protected float mMaxOverScrollAngle; // This will be updated from values.xml 177 private float mMaxOverScrollSlide; 178 179 // Drag Lock 180 private @DragLock int mDragLock = DragLock.NONE; 181 private long mLastScrollUpdate; 182 private float mMinScrollMotion; 183 184 // Scrolling Variables 185 protected float mScrollTarget; 186 protected float mScrollOffset; 187 private float mScrollOffsetForDyingTabs; 188 protected float mCurrentScrollDirection; 189 protected StackTab mScrollingTab; 190 191 // Swipe Variables 192 private float mSwipeUnboundScrollOffset; 193 private float mSwipeBoundedScrollOffset; 194 private boolean mSwipeIsCancelable; 195 private boolean mSwipeCanScroll; 196 protected boolean mInSwipe; 197 198 // Discard 199 protected StackTab mDiscardingTab; 200 201 // We can't initialize mDiscardDirection here using LocalizationUtils.isRtl() because it 202 // will involve a jni call. Instead, mDiscardDirection will be initialized in Show(). 203 private float mDiscardDirection = Float.NaN; 204 205 private int mReferenceOrderIndex = -1; 206 207 // Orientation Variables 208 protected @Orientation int mCurrentMode = Orientation.PORTRAIT; 209 210 // Animation Variables 211 protected @OverviewAnimationType int mOverviewAnimationType = OverviewAnimationType.NONE; 212 private StackAnimation mAnimationFactory; 213 private StackViewAnimation mViewAnimationFactory; 214 215 // Running set of animations applied to tabs. 216 private StackAnimation.StackAnimatorSet mStackAnimatorSet; 217 private Animator mViewAnimations; 218 219 // The parent Layout 220 protected final StackLayoutBase mLayout; 221 222 // Border values 223 protected float mBorderTransparentTop; 224 protected float mBorderTransparentSide; 225 // TODO(dtrainor): Expose 9-patch padding from resource manager. 226 protected float mBorderTopPadding; 227 private float mBorderLeftPadding; 228 229 // The slop amount in dp to detect a touch on the tab. Cached values from values/dimens.xml. 230 private float mCompositorButtonSlop; // compositor_button_slop 231 232 private boolean mIsStackForCurrentTabList; 233 234 private final AnimatorListenerAdapter mViewAnimatorListener = new AnimatorListenerAdapter() { 235 @Override 236 public void onAnimationCancel(Animator animation) { 237 mLayout.requestUpdate(); 238 } 239 240 @Override 241 public void onAnimationEnd(Animator animation) { 242 mLayout.requestUpdate(); 243 } 244 }; 245 246 /** 247 * @param layout The parent layout. 248 */ Stack(Context context, StackLayoutBase layout)249 public Stack(Context context, StackLayoutBase layout) { 250 mLayout = layout; 251 contextChanged(context); 252 } 253 254 /** 255 * @return Animation handler associated with this stack. 256 */ getAnimationHandler()257 public CompositorAnimationHandler getAnimationHandler() { 258 return mLayout.getAnimationHandler(); 259 } 260 261 /** 262 * @param tabList The list to attach to this stack. 263 */ setTabList(TabList tabList)264 public void setTabList(TabList tabList) { 265 mTabList = tabList; 266 } 267 268 /** 269 * @return The TabList associated with this stack. 270 */ getTabList()271 public TabList getTabList() { 272 return mTabList; 273 } 274 275 /** 276 * @return The {@link StackTab}s currently being rendered by the tab stack. 277 * @VisibleForTesting 278 */ getTabs()279 public StackTab[] getTabs() { 280 return mStackTabs; 281 } 282 283 /** 284 * @return The number of tabs in the tab stack. 285 * @VisibleForTesting 286 */ getCount()287 public int getCount() { 288 return mStackTabs != null ? mStackTabs.length : 0; 289 } 290 291 /** 292 * @return The number of visible tabs in the tab stack. 293 */ getVisibleCount()294 public int getVisibleCount() { 295 int visibleCount = 0; 296 if (mStackTabs != null) { 297 for (int i = 0; i < mStackTabs.length; ++i) { 298 if (mStackTabs[i].getLayoutTab().isVisible()) visibleCount++; 299 } 300 } 301 return visibleCount; 302 } 303 304 /** 305 * The scale the tabs should be currently shown at (may change based on how many are open). 306 */ getScaleAmount()307 public abstract float getScaleAmount(); 308 309 /* 310 * Main Interaction Methods for the rest of the application 311 * 312 * 313 * These methods are the main entry points for the model to tell the 314 * view that something has changed. The rest of the application can 315 * alert this class that something in the tab stack has changed or that 316 * the user has decided to enter the tab switcher. 317 * 318 */ 319 320 /** 321 * Triggers the closing motions. 322 * 323 * @param time The current time of the app in ms. 324 * @param id The id of the tab that get closed. 325 */ tabClosingEffect(long time, int id)326 public void tabClosingEffect(long time, int id) { 327 if (mStackTabs == null) return; 328 329 // |id| cannot be used to access the particular tab in the model. 330 // The tab is already gone from the model by this point. 331 332 int newIndex = 0; 333 boolean needAnimation = false; 334 for (int i = 0; i < mStackTabs.length; ++i) { 335 if (mStackTabs[i].getId() == id) { 336 // Mark the {@link StackTab} as dying so that when the animation is 337 // finished we can clear it out of the stack. This supports 338 // multiple {@link StackTab} deletions. 339 needAnimation |= !mStackTabs[i].isDying(); 340 mStackTabs[i].setDying(true); 341 } else { 342 // Update the {@link StackTab} with a new index here. This makes sure the 343 // {@link LayoutTab} end up in the proper place. 344 mStackTabs[i].setNewIndex(newIndex++); 345 } 346 } 347 348 if (needAnimation) { 349 mScrollOffsetForDyingTabs = mScrollOffset; 350 mSpacing = computeSpacing(newIndex); 351 352 startAnimation(time, OverviewAnimationType.DISCARD); 353 } 354 355 if (newIndex == 0) mIsDying = true; 356 } 357 358 /** 359 * @return True if we should put the close button on the right side of the tab, or false if 360 * we should put it on the left. This method already accounts for RTL flipping. 361 */ isCloseButtonOnRight()362 private boolean isCloseButtonOnRight() { 363 if (ChromeFeatureList.isEnabled(ChromeFeatureList.HORIZONTAL_TAB_SWITCHER_ANDROID)) { 364 return !LocalizationUtils.isLayoutRtl(); 365 } 366 367 return mCurrentMode == Orientation.PORTRAIT ^ LocalizationUtils.isLayoutRtl(); 368 } 369 370 /** 371 * Animates all the tabs closing at once. 372 * 373 * @param time The current time of the app in ms. 374 */ tabsAllClosingEffect(long time)375 public void tabsAllClosingEffect(long time) { 376 boolean needAnimation = false; 377 378 if (mStackTabs != null) { 379 for (int i = 0; i < mStackTabs.length; ++i) { 380 needAnimation |= !mStackTabs[i].isDying(); 381 mStackTabs[i].setDying(true); 382 } 383 } else { 384 // This needs to be set to true to handle the case where both the normal and 385 // incognito tabs are being closed. 386 needAnimation = true; 387 } 388 389 if (needAnimation) { 390 mScrollOffsetForDyingTabs = mScrollOffset; 391 mSpacing = computeSpacing(0); 392 393 if (mStackTabs != null) { 394 for (int i = 0; i < mStackTabs.length; i++) { 395 StackTab tab = mStackTabs[i]; 396 tab.setDiscardOriginY(0.f); 397 tab.setDiscardOriginX(isCloseButtonOnRight() 398 ? tab.getLayoutTab().getOriginalContentWidth() 399 : 0.f); 400 tab.setDiscardFromClick(true); 401 } 402 } 403 startAnimation(time, OverviewAnimationType.DISCARD_ALL); 404 } 405 406 mIsDying = true; 407 } 408 409 /** 410 * Animates a new tab opening. 411 * 412 * @param time The current time of the app in ms. 413 * @param id The id of the new tab to animate. 414 */ tabCreated(long time, int id)415 public void tabCreated(long time, int id) { 416 if (!createTabHelper(id)) return; 417 mIsDying = false; 418 419 finishAnimation(time); 420 startAnimation(time, OverviewAnimationType.NEW_TAB_OPENED, 421 TabModelUtils.getTabIndexById(mTabList, id), TabList.INVALID_TAB_INDEX, false); 422 } 423 424 /** 425 * Animates the closing of the stack. Focusing on the selected tab. 426 * 427 * @param time The current time of the app in ms. 428 * @param id The id of the tab to select. 429 */ tabSelectingEffect(long time, int id)430 public void tabSelectingEffect(long time, int id) { 431 int index = TabModelUtils.getTabIndexById(mTabList, id); 432 startAnimation(time, OverviewAnimationType.TAB_FOCUSED, index, -1, false); 433 } 434 435 /** 436 * Called set up the tab stack to the initial state when it is entered. 437 * 438 * @param time The current time of the app in ms. 439 * @param focused Whether or not the stack was focused when entering. 440 */ stackEntered(long time, boolean focused)441 public void stackEntered(long time, boolean focused) { 442 // Don't request new thumbnails until the animation is over. We should 443 // have cached the visible ones already. 444 boolean finishImmediately = !focused; 445 mSpacing = computeSpacing(mStackTabs != null ? mStackTabs.length : 0); 446 resetAllScrollOffset(); 447 startAnimation(time, OverviewAnimationType.ENTER_STACK, finishImmediately); 448 } 449 450 /** 451 * @return Whether or not the TabList represented by this TabStackState should be displayed. 452 */ isDisplayable()453 public boolean isDisplayable() { 454 if (mTabList == null) return false; 455 456 return !mTabList.isIncognito() || (!mIsDying && mTabList.getCount() > 0); 457 } 458 getDefaultDiscardDirection()459 private float getDefaultDiscardDirection() { 460 return (mCurrentMode == Orientation.LANDSCAPE && LocalizationUtils.isLayoutRtl()) ? -1.0f 461 : 1.0f; 462 } 463 464 /** 465 * show is called to set up the initial variables, and must always be called before 466 * displaying the stack. 467 * @param isStackForCurrentTabList Whether this {@link Stack} is for the current tab list. 468 */ show(boolean isStackForCurrentTabList)469 public void show(boolean isStackForCurrentTabList) { 470 mIsStackForCurrentTabList = isStackForCurrentTabList; 471 472 mDiscardDirection = getDefaultDiscardDirection(); 473 474 // Reinitialize the roll over counter for each tabswitcher session. 475 mOverScrollCounter = 0; 476 477 // TODO: Recreating the stack {@link StackTab} here might be overkill. Will these 478 // already exist in the cache? Check to make sure it makes sense. 479 createStackTabs(false); 480 } 481 482 /* 483 * Animation Start and Finish Methods 484 * 485 * This method kicks off animations by using the 486 * TabSwitcherAnimationFactory to create an AnimatorSet. 487 */ 488 489 /** 490 * Starts an animation on the stack. 491 * 492 * @param time The current time of the app in ms. 493 * @param type The type of the animation to start. 494 */ startAnimation(long time, @OverviewAnimationType int type)495 protected void startAnimation(long time, @OverviewAnimationType int type) { 496 startAnimation(time, type, TabList.INVALID_TAB_INDEX, false); 497 } 498 499 /** 500 * Starts an animation on the stack. 501 * 502 * @param time The current time of the app in ms. 503 * @param type The type of the animation to start. 504 * @param finishImmediately Whether the animation jumps straight to the end. 505 */ startAnimation( long time, @OverviewAnimationType int type, boolean finishImmediately)506 private void startAnimation( 507 long time, @OverviewAnimationType int type, boolean finishImmediately) { 508 startAnimation(time, type, TabList.INVALID_TAB_INDEX, finishImmediately); 509 } 510 511 /** 512 * Starts an animation on the stack. 513 * 514 * @param time The current time of the app in ms. 515 * @param type The type of the animation to start. 516 * @param sourceIndex The source index needed by some animation types. 517 * @param finishImmediately Whether the animation jumps straight to the end. 518 */ startAnimation(long time, @OverviewAnimationType int type, int sourceIndex, boolean finishImmediately)519 protected void startAnimation(long time, @OverviewAnimationType int type, int sourceIndex, 520 boolean finishImmediately) { 521 startAnimation(time, type, mTabList.index(), sourceIndex, finishImmediately); 522 } 523 startAnimation(long time, @OverviewAnimationType int type, int focusIndex, int sourceIndex, boolean finishImmediately)524 private void startAnimation(long time, @OverviewAnimationType int type, int focusIndex, 525 int sourceIndex, boolean finishImmediately) { 526 if (!canUpdateAnimation(time, type, sourceIndex, finishImmediately)) { 527 // We need to finish animations started earlier before we start 528 // off a new one. 529 finishAnimation(time); 530 // Stop movement while the animation takes place. 531 stopScrollingMovement(time); 532 } 533 534 if (mAnimationFactory != null && mViewAnimationFactory != null) { 535 mOverviewAnimationType = type; 536 537 // First try to build a View animation. Then fallback to the compositor animation 538 // if one isn't created. 539 mViewAnimations = mViewAnimationFactory.createAnimatorForType( 540 type, mStackTabs, mLayout.getViewContainer(), mTabList, focusIndex); 541 542 if (mViewAnimations != null) { 543 mViewAnimations.addListener(mViewAnimatorListener); 544 } else { 545 // Build the AnimatorSet using the TabSwitcherAnimationFactory. 546 // This will give us the appropriate AnimatorSet based on the current 547 // state of the tab switcher and the OverviewAnimationType specified. 548 mStackAnimatorSet = mAnimationFactory.createAnimatorSetForType(type, this, 549 mStackTabs, focusIndex, sourceIndex, mSpacing, getDiscardRange()); 550 } 551 552 if (mStackAnimatorSet != null) mStackAnimatorSet.start(); 553 if (mViewAnimations != null) mViewAnimations.start(); 554 if (mStackAnimatorSet != null || mViewAnimations != null) { 555 mLayout.onStackAnimationStarted(); 556 } 557 558 if ((mStackAnimatorSet == null && mViewAnimations == null) || finishImmediately) { 559 finishAnimation(time); 560 } 561 } 562 563 mLayout.requestUpdate(); 564 } 565 566 /** 567 * Performs the necessary actions to finish the current animation. 568 * 569 * @param time The current time of the app in ms. 570 */ finishAnimation(long time)571 protected void finishAnimation(long time) { 572 if (mStackAnimatorSet != null) mStackAnimatorSet.end(); 573 if (mViewAnimations != null) mViewAnimations.end(); 574 if (mStackAnimatorSet != null || mViewAnimations != null) { 575 mLayout.onStackAnimationFinished(); 576 } 577 578 switch (mOverviewAnimationType) { 579 case OverviewAnimationType.ENTER_STACK: 580 mLayout.uiDoneEnteringStack(); 581 break; 582 case OverviewAnimationType.FULL_ROLL: 583 for (int i = 0; i < mStackTabs.length; i++) { 584 mStackTabs[i].getLayoutTab().setTiltX(0, 0); 585 mStackTabs[i].getLayoutTab().setTiltY(0, 0); 586 } 587 springBack(time); 588 break; 589 case OverviewAnimationType.TAB_FOCUSED: 590 // Purposeful fall through 591 case OverviewAnimationType.NEW_TAB_OPENED: 592 // Nothing to do. 593 break; 594 case OverviewAnimationType.DISCARD_ALL: 595 mLayout.uiDoneClosingAllTabs(mTabList.isIncognito()); 596 cleanupStackTabState(); 597 break; 598 case OverviewAnimationType.UNDISCARD: 599 // Purposeful fall through because if UNDISCARD animation updated DISCARD animation, 600 // DISCARD animation clean up below is not called so UNDISCARD is responsible for 601 // cleaning it up. 602 case OverviewAnimationType.DISCARD: 603 // Remove all dying tabs from mStackTabs. 604 if (mStackTabs != null) { 605 // Request for the model to be updated. 606 for (int i = 0; i < mStackTabs.length; ++i) { 607 StackTab tab = mStackTabs[i]; 608 if (tab.isDying()) { 609 mLayout.uiDoneClosingTab( 610 time, tab.getId(), true, mTabList.isIncognito()); 611 } 612 } 613 } 614 cleanupStackTabState(); 615 break; 616 default: 617 break; 618 } 619 620 // sync the scrollTarget and scrollOffset. For ENTER_STACK animation, don't sync to 621 // ensure the tab can tilt back. 622 if (mOverviewAnimationType != OverviewAnimationType.NONE 623 && mOverviewAnimationType != OverviewAnimationType.ENTER_STACK 624 && mScroller.isFinished()) { 625 setScrollTarget(mScrollOffset, true); 626 } 627 mOverviewAnimationType = OverviewAnimationType.NONE; 628 629 mStackAnimatorSet = null; 630 mViewAnimations = null; 631 } 632 cleanupStackTabState()633 private void cleanupStackTabState() { 634 if (mStackTabs != null) { 635 // First count the number of tabs that are still alive. 636 int nNumberOfLiveTabs = 0; 637 for (int i = 0; i < mStackTabs.length; ++i) { 638 if (mStackTabs[i].isDying()) { 639 mLayout.releaseTabLayout(mStackTabs[i].getLayoutTab()); 640 } else { 641 nNumberOfLiveTabs++; 642 } 643 } 644 645 if (nNumberOfLiveTabs == 0) { 646 // We have no more live {@link StackTab}. Just clean all tab related states. 647 cleanupTabs(); 648 } else if (nNumberOfLiveTabs < mStackTabs.length) { 649 // If any tabs have died, we need to remove them from mStackTabs. 650 651 StackTab[] oldTabs = mStackTabs; 652 mStackTabs = new StackTab[nNumberOfLiveTabs]; 653 654 int newIndex = 0; 655 for (int i = 0; i < oldTabs.length; ++i) { 656 if (!oldTabs[i].isDying()) { 657 mStackTabs[newIndex] = oldTabs[i]; 658 mStackTabs[newIndex].setNewIndex(newIndex); 659 newIndex++; 660 } 661 } 662 assert newIndex == nNumberOfLiveTabs; 663 } 664 } 665 666 mDiscardDirection = getDefaultDiscardDirection(); 667 } 668 669 /** 670 * Ensure that there are no dying tabs by finishing the current animation. 671 * 672 * @param time The current time of the app in ms. 673 */ ensureCleaningUpDyingTabs(long time)674 public void ensureCleaningUpDyingTabs(long time) { 675 finishAnimation(time); 676 } 677 678 /** 679 * Decide if the animation can be started without cleaning up the current animation. 680 * @param time The current time of the app in ms. 681 * @param type The type of the animation to start. 682 * @param sourceIndex The source index needed by some animation types. 683 * @param finishImmediately Whether the animation jumps straight to the end. 684 * @return true, if we can start the animation without cleaning up the 685 * current animation. 686 */ canUpdateAnimation(long time, @OverviewAnimationType int type, int sourceIndex, boolean finishImmediately)687 private boolean canUpdateAnimation(long time, @OverviewAnimationType int type, int sourceIndex, 688 boolean finishImmediately) { 689 if (mAnimationFactory != null) { 690 if ((mOverviewAnimationType == OverviewAnimationType.DISCARD 691 || mOverviewAnimationType == OverviewAnimationType.UNDISCARD 692 || mOverviewAnimationType == OverviewAnimationType.DISCARD_ALL) 693 && (type == OverviewAnimationType.DISCARD 694 || type == OverviewAnimationType.UNDISCARD 695 || type == OverviewAnimationType.DISCARD_ALL)) { 696 return true; 697 } 698 } 699 return false; 700 } 701 702 /** 703 * Cancel scrolling animation which is a part of discarding animation. 704 * @return true if the animation is canceled, false, if there is nothing to cancel. 705 */ cancelDiscardScrollingAnimation()706 private boolean cancelDiscardScrollingAnimation() { 707 if (mOverviewAnimationType == OverviewAnimationType.DISCARD 708 || mOverviewAnimationType == OverviewAnimationType.UNDISCARD 709 || mOverviewAnimationType == OverviewAnimationType.DISCARD_ALL) { 710 if (mStackAnimatorSet != null) { 711 mStackAnimatorSet.cancelCancelableAnimators(); 712 } 713 return true; 714 } 715 return false; 716 } 717 718 /** 719 * Checks any Android view animations to see if they have finished yet. 720 * @param time The current time of the app in ms. 721 * @param jumpToEnd Whether to finish the animation. 722 * @return Whether the animation was finished. 723 */ onUpdateViewAnimation(long time, boolean jumpToEnd)724 public boolean onUpdateViewAnimation(long time, boolean jumpToEnd) { 725 boolean finished = true; 726 if (mViewAnimations != null) { 727 finished = !mViewAnimations.isRunning(); 728 finishAnimationsIfDone(time, jumpToEnd); 729 } 730 return finished; 731 } 732 733 /** 734 * Steps the animation forward and updates all the animated values. 735 * @param time The current time of the app in ms. 736 * @param jumpToEnd Whether to finish the animation. 737 * @return Whether the animation was finished. 738 */ onUpdateCompositorAnimations(long time, boolean jumpToEnd)739 public boolean onUpdateCompositorAnimations(long time, boolean jumpToEnd) { 740 if (!jumpToEnd) updateScrollOffset(time); 741 742 boolean animatorSetFinished = true; 743 if (mStackAnimatorSet != null) { 744 animatorSetFinished = jumpToEnd ? true : !mStackAnimatorSet.isRunning(); 745 } 746 747 if (mStackAnimatorSet != null) finishAnimationsIfDone(time, jumpToEnd); 748 if (jumpToEnd) forceScrollStop(); 749 750 return animatorSetFinished; 751 } 752 finishAnimationsIfDone(long time, boolean jumpToEnd)753 private void finishAnimationsIfDone(long time, boolean jumpToEnd) { 754 boolean hasViewAnimations = mViewAnimations != null; 755 boolean isViewFinished = hasViewAnimations ? !mViewAnimations.isRunning() : true; 756 757 boolean hasAnimatorSetTabAnimations = mStackAnimatorSet != null; 758 boolean isAnimatorSetTabFinished = 759 hasAnimatorSetTabAnimations ? !mStackAnimatorSet.isRunning() : true; 760 761 boolean hasAnimations = hasViewAnimations || hasAnimatorSetTabAnimations; 762 763 boolean shouldFinish = jumpToEnd && hasAnimations; 764 shouldFinish |= hasAnimations && (!hasViewAnimations || isViewFinished) 765 && (!hasAnimatorSetTabAnimations || isAnimatorSetTabFinished); 766 767 if (shouldFinish) finishAnimation(time); 768 } 769 770 /** 771 * Determines which action was specified by the user's drag. 772 * 773 * @param scrollDrag The number of pixels moved in the scroll direction. 774 * @param discardDrag The number of pixels moved in the discard direction. 775 * @return The current lock mode or a hint if the motion was not strong enough 776 * to fully lock the mode. 777 */ computeDragLock(float scrollDrag, float discardDrag)778 private @DragLock int computeDragLock(float scrollDrag, float discardDrag) { 779 scrollDrag = Math.abs(scrollDrag); 780 discardDrag = Math.abs(discardDrag); 781 @DragLock 782 int hintLock = (discardDrag * DRAG_ANGLE_THRESHOLD) > scrollDrag ? DragLock.DISCARD 783 : DragLock.SCROLL; 784 // If the user paused the drag for too long, re-determine what the new action is. 785 long timeMillisecond = System.currentTimeMillis(); 786 if ((timeMillisecond - mLastScrollUpdate) > DRAG_TIME_THRESHOLD) { 787 mDragLock = DragLock.NONE; 788 } 789 // Select the scroll lock if enough conviction is put into scrolling. 790 if ((mDragLock == DragLock.NONE && Math.abs(scrollDrag - discardDrag) > mMinScrollMotion) 791 || (mDragLock == DragLock.DISCARD && discardDrag > mMinScrollMotion) 792 || (mDragLock == DragLock.SCROLL && scrollDrag > mMinScrollMotion)) { 793 mLastScrollUpdate = timeMillisecond; 794 if (mDragLock == DragLock.NONE) { 795 mDragLock = hintLock; 796 } 797 } 798 // Returns a hint of the lock so we can show feedback even if the lock is not committed 799 // yet. 800 return mDragLock == DragLock.NONE ? hintLock : mDragLock; 801 } 802 803 /* 804 * User Input Routines: 805 * 806 * The input routines that process gestures and click touches. These 807 * are the main way to interact with the view directly. Other input 808 * paths happen when model changes impact the view. This can happen 809 * as a result of some of these actions or from other user input (ie: 810 * from the Toolbar). These are ignored if an animation is currently 811 * in progress. 812 */ 813 814 /** 815 * Called on drag event (from scroll events in the gesture detector). 816 * 817 * @param time The current time of the app in ms. 818 * @param x The x coordinate of the end of the drag event. 819 * @param y The y coordinate of the end of the drag event. 820 * @param amountX The number of pixels dragged in the x direction since the last event. 821 * @param amountY The number of pixels dragged in the y direction since the last event. 822 */ drag(long time, float x, float y, float amountX, float amountY)823 public void drag(long time, float x, float y, float amountX, float amountY) { 824 float scrollDrag; 825 float discardDrag; 826 if (mCurrentMode == Orientation.PORTRAIT) { 827 discardDrag = amountX; 828 scrollDrag = amountY; 829 } else { 830 discardDrag = amountY; 831 scrollDrag = LocalizationUtils.isLayoutRtl() ? -amountX : amountX; 832 } 833 @DragLock 834 int hintLock = computeDragLock(scrollDrag, discardDrag); 835 if (hintLock == DragLock.DISCARD) { 836 discard(x, y, amountX, amountY); 837 } else { 838 // Only cancel the current discard attempt if the scroll lock is committed: 839 // by using mDragLock instead of hintLock. 840 if (mDragLock == DragLock.SCROLL && mDiscardingTab != null) { 841 commitDiscard(time, false); 842 } 843 scroll(x, y, LocalizationUtils.isLayoutRtl() ? -amountX : amountX, amountY, false); 844 } 845 mLayout.requestUpdate(); 846 } 847 848 /** 849 * Discards and updates the position based on the input event values. 850 * 851 * @param x The x coordinate of the end of the drag event. 852 * @param y The y coordinate of the end of the drag event. 853 * @param amountX The number of pixels dragged in the x direction since the last event. 854 * @param amountY The number of pixels dragged in the y direction since the last event. 855 */ discard(float x, float y, float amountX, float amountY)856 private void discard(float x, float y, float amountX, float amountY) { 857 if (mStackTabs == null 858 || (mOverviewAnimationType != OverviewAnimationType.NONE 859 && mOverviewAnimationType != OverviewAnimationType.DISCARD 860 && mOverviewAnimationType != OverviewAnimationType.DISCARD_ALL 861 && mOverviewAnimationType != OverviewAnimationType.UNDISCARD)) { 862 return; 863 } 864 865 if (mDiscardingTab == null) { 866 if (!mInSwipe) { 867 mDiscardingTab = getTabAtPositon(x, y); 868 } else { 869 if (mTabList.index() < 0) return; 870 mDiscardingTab = mStackTabs[mTabList.index()]; 871 } 872 873 if (mDiscardingTab != null) { 874 cancelDiscardScrollingAnimation(); 875 876 // Make sure we are well within the tab in the discard direction. 877 RectF target = getClickTargetBoundsForLayoutTab(mDiscardingTab.getLayoutTab()); 878 float distanceToEdge; 879 float edgeToEdge; 880 if (mCurrentMode == Orientation.PORTRAIT) { 881 mDiscardDirection = 1.0f; 882 distanceToEdge = Math.max(target.left - x, x - target.right); 883 edgeToEdge = target.width(); 884 } else { 885 mDiscardDirection = 2.0f - 4.0f * (x / mLayout.getWidth()); 886 mDiscardDirection = MathUtils.clamp(mDiscardDirection, -1.0f, 1.0f); 887 distanceToEdge = Math.max(target.top - y, y - target.bottom); 888 edgeToEdge = target.height(); 889 } 890 891 float scaledDiscardX = x - mDiscardingTab.getLayoutTab().getX(); 892 float scaledDiscardY = y - mDiscardingTab.getLayoutTab().getY(); 893 mDiscardingTab.setDiscardOriginX(scaledDiscardX / mDiscardingTab.getScale()); 894 mDiscardingTab.setDiscardOriginY(scaledDiscardY / mDiscardingTab.getScale()); 895 mDiscardingTab.setDiscardFromClick(false); 896 897 if (Math.abs(distanceToEdge) < DISCARD_SAFE_SELECTION_PCTG * edgeToEdge) { 898 mDiscardingTab = null; 899 } 900 } 901 } 902 if (mDiscardingTab != null) { 903 float deltaAmount = mCurrentMode == Orientation.PORTRAIT ? amountX : amountY; 904 mDiscardingTab.addToDiscardAmount(deltaAmount); 905 } 906 } 907 908 /** 909 * Called on touch/tilt scroll event. 910 * 911 * @param x The x coordinate of the end of the scroll event. 912 * @param y The y coordinate of the end of the scroll event. 913 * @param amountX The number of pixels scrolled in the x direction. 914 * @param amountY The number of pixels scrolled in the y direction. 915 * @param isTilt True if the call comes from a tilt event. 916 */ scroll(float x, float y, float amountX, float amountY, boolean isTilt)917 private void scroll(float x, float y, float amountX, float amountY, boolean isTilt) { 918 if ((!mScroller.isFinished() && isTilt) || mStackTabs == null 919 || (mOverviewAnimationType != OverviewAnimationType.NONE 920 && mOverviewAnimationType != OverviewAnimationType.DISCARD 921 && mOverviewAnimationType != OverviewAnimationType.UNDISCARD 922 && mOverviewAnimationType != OverviewAnimationType.DISCARD_ALL 923 && mOverviewAnimationType != OverviewAnimationType.ENTER_STACK)) { 924 return; 925 } 926 927 float amountScreen = mCurrentMode == Orientation.PORTRAIT ? amountY : amountX; 928 float amountScroll = amountScreen; 929 float amountEvenOut = amountScreen; 930 931 // Computes the right amount for the scrolling so the finger matches the tab under it. 932 float tabScrollSpaceFinal = 0; 933 if (mScrollingTab == null || isTilt) { 934 mScrollingTab = getTabAtPositon(x, y); 935 } 936 937 if (mScrollingTab == null && mInSwipe && mStackTabs != null) { 938 int index = mTabList.index(); 939 if (index >= 0 && index <= mStackTabs.length) mScrollingTab = mStackTabs[index]; 940 } 941 942 if (mScrollingTab == null) { 943 if (!isTilt) { 944 amountScroll = 0; 945 amountEvenOut = 0; 946 } 947 } else if (mScrollingTab.getIndex() == 0) { 948 amountEvenOut = 0; 949 } else { 950 // Find the scroll that make the selected tab move the right 951 // amount on the screen. 952 float tabScrollSpace = mScrollingTab.getScrollOffset() + mScrollOffset; 953 float tabScreen = scrollToScreen(tabScrollSpace); 954 tabScrollSpaceFinal = screenToScroll(tabScreen + amountScreen); 955 amountScroll = tabScrollSpaceFinal - tabScrollSpace; 956 // Matching the finger is too strong of a constraints on the edges. So we make 957 // sure the end value is not too far from the linear case. 958 amountScroll = Math.signum(amountScreen) 959 * MathUtils.clamp(Math.abs(amountScroll), Math.abs(amountScreen) * 0.5f, 960 Math.abs(amountScreen) * 2.0f); 961 } 962 963 // Evens out the tabs and correct the scroll amount if needed. 964 if (evenOutTabs(amountEvenOut, false) && mScrollingTab.getIndex() > 0) { 965 // Adjust the amount after the even phase 966 float tabScrollSpace = mScrollingTab.getScrollOffset() + mScrollOffset; 967 amountScroll = tabScrollSpaceFinal - tabScrollSpace; 968 } 969 970 // Actually do the scrolling. 971 setScrollTarget(mScrollTarget + amountScroll, false); 972 } 973 974 /** 975 * OverlappingStack implements this to auto-magically the cards as the stack get scrolled. 976 * NonOverlappingStack just ignores this call. 977 * 978 * @param amount The amount of scroll performed in pixel. The sign indicates 979 * the direction. 980 * @param allowReverseDirection Whether or not to allow corrections in the reverse direction 981 * of the amount scrolled. 982 * @return True if any tab had been 'visibly' moved. 983 */ evenOutTabs(float amount, boolean allowReverseDirection)984 protected abstract boolean evenOutTabs(float amount, boolean allowReverseDirection); 985 986 /** 987 * Called on touch fling event. Scroll the stack or help to discard a tab. 988 * 989 * @param time The current time of the app in ms. 990 * @param x The y coordinate of the start of the fling event. 991 * @param y The y coordinate of the start of the fling event. 992 * @param velocityX The amount of velocity in the x direction. 993 * @param velocityY The amount of velocity in the y direction. 994 */ fling(long time, float x, float y, float velocityX, float velocityY)995 public void fling(long time, float x, float y, float velocityX, float velocityY) { 996 if (mDragLock != DragLock.SCROLL && mDiscardingTab != null) { 997 float velocity = mCurrentMode == Orientation.PORTRAIT ? velocityX : velocityY; 998 float maxDelta = getDiscardRange() * DISCARD_FLING_MAX_CONTRIBUTION; 999 float deltaAmount = MathUtils.clamp(velocity * DISCARD_FLING_DT, -maxDelta, maxDelta); 1000 mDiscardingTab.addToDiscardAmount(deltaAmount); 1001 } else if (mOverviewAnimationType == OverviewAnimationType.NONE && mScroller.isFinished() 1002 && mOverScrollOffset == 0 && getTabIndexAtPositon(x, y) >= 0) { 1003 float velocity = mCurrentMode == Orientation.PORTRAIT 1004 ? velocityY 1005 : (LocalizationUtils.isLayoutRtl() ? -velocityX : velocityX); 1006 // Fling only overscrolls when the stack is fully unfolded. 1007 mScroller.fling(0, (int) mScrollTarget, 0, (int) velocity, 0, 0, 1008 (int) getMinScroll(false), (int) getMaxScroll(false), 0, 1009 (int) ((velocity > 0 ? mMaxOverScroll : mMaxUnderScroll) 1010 * MAX_OVER_FLING_SCALE), 1011 time); 1012 1013 // Set the target to the final scroll position to make sure 1014 // the offset finally gets there regardless of what happens. 1015 // We override this when the user interrupts the fling though. 1016 setScrollTarget(mScroller.getFinalY(), false); 1017 } 1018 } 1019 1020 /** 1021 * Get called on down touch event. 1022 * 1023 * @param time The current time of the app in ms. 1024 */ onDown(long time)1025 public void onDown(long time) { 1026 mDragLock = DragLock.NONE; 1027 if (mOverviewAnimationType == OverviewAnimationType.NONE) { 1028 stopScrollingMovement(time); 1029 } 1030 // Resets the scrolling state. 1031 mScrollingTab = null; 1032 commitDiscard(time, false); 1033 } 1034 1035 /** 1036 * Get called on long press touch event. 1037 * 1038 * @param time The current time of the app in ms. 1039 * @param x The x coordinate in pixel inside the stack view. 1040 * @param y The y coordinate in pixel inside the stack view. 1041 */ onLongPress(long time, float x, float y)1042 public abstract void onLongPress(long time, float x, float y); 1043 1044 /** 1045 * Called when at least 2 touch events are detected. 1046 * 1047 * @param time The current time of the app in ms. 1048 * @param x0 The x coordinate of the first touch event. 1049 * @param y0 The y coordinate of the first touch event. 1050 * @param x1 The x coordinate of the second touch event. 1051 * @param y1 The y coordinate of the second touch event. 1052 * @param firstEvent The pinch is the first of a sequence of pinch events. 1053 */ onPinch( long time, float x0, float y0, float x1, float y1, boolean firstEvent)1054 public abstract void onPinch( 1055 long time, float x0, float y0, float x1, float y1, boolean firstEvent); 1056 1057 /** 1058 * Commits or release the that currently being considered for discard. This function 1059 * also triggers the associated animations. 1060 * 1061 * @param time The current time of the app in ms. 1062 * @param allowDiscard Whether to allow to discard the tab currently being considered 1063 * for discard. 1064 */ commitDiscard(long time, boolean allowDiscard)1065 protected void commitDiscard(long time, boolean allowDiscard) { 1066 if (mDiscardingTab == null) return; 1067 1068 assert mStackTabs != null; 1069 StackTab discarded = mDiscardingTab; 1070 if (Math.abs(discarded.getDiscardAmount()) / getDiscardRange() > DISCARD_COMMIT_THRESHOLD 1071 && allowDiscard) { 1072 mLayout.uiRequestingCloseTab(time, discarded.getId()); 1073 RecordUserAction.record("MobileStackViewSwipeCloseTab"); 1074 RecordUserAction.record("MobileTabClosed"); 1075 } else { 1076 startAnimation(time, OverviewAnimationType.UNDISCARD); 1077 } 1078 mDiscardingTab = null; 1079 mLayout.requestUpdate(); 1080 } 1081 1082 /** 1083 * Called on touch up or cancel event. 1084 */ onUpOrCancel(long time)1085 public void onUpOrCancel(long time) { 1086 // Commit or uncommit discard tab 1087 commitDiscard(time, true); 1088 1089 resetInputActionIndices(); 1090 1091 springBack(time); 1092 } 1093 1094 /** 1095 * Bounces the scroll position back to a valid value (e.g. to correct an overscroll or 1096 * implement snapping). 1097 */ springBack(long time)1098 protected abstract void springBack(long time); 1099 1100 /** 1101 * Called on touch click event. 1102 * 1103 * @param time The current time of the app in ms. 1104 * @param x The x coordinate in pixel inside the stack view. 1105 * @param y The y coordinate in pixel inside the stack view. 1106 */ click(long time, float x, float y)1107 public void click(long time, float x, float y) { 1108 if (mOverviewAnimationType != OverviewAnimationType.NONE 1109 && mOverviewAnimationType != OverviewAnimationType.DISCARD 1110 && mOverviewAnimationType != OverviewAnimationType.UNDISCARD 1111 && mOverviewAnimationType != OverviewAnimationType.DISCARD_ALL) { 1112 return; 1113 } 1114 int clicked = getTabIndexAtPositon(x, y, mCompositorButtonSlop); 1115 if (clicked >= 0) { 1116 // Check if the click was within the boundaries of the close button defined by its 1117 // visible coordinates. 1118 if (checkCloseHitTestOnLayoutTab(x, y, mStackTabs[clicked].getLayoutTab())) { 1119 // Tell the model to close the tab because the close button was pressed. The 1120 // model will then trigger a notification which will start the actual close 1121 // process here if necessary. 1122 StackTab tab = mStackTabs[clicked]; 1123 final float halfCloseBtnWidth = LayoutTab.CLOSE_BUTTON_WIDTH_DP / 2.f; 1124 final float halfCloseBtnHeight = mBorderTopPadding / 2.f; 1125 final float contentWidth = tab.getLayoutTab().getOriginalContentWidth(); 1126 1127 tab.setDiscardOriginY(halfCloseBtnHeight); 1128 tab.setDiscardOriginX(isCloseButtonOnRight() ? contentWidth - halfCloseBtnWidth 1129 : halfCloseBtnWidth); 1130 tab.setDiscardFromClick(true); 1131 mLayout.uiRequestingCloseTab(time, tab.getId()); 1132 RecordUserAction.record("MobileStackViewCloseTab"); 1133 RecordUserAction.record("MobileTabClosed"); 1134 } else { 1135 // Let the model know that a new {@link LayoutTab} was selected. The model will 1136 // notify us if we need to do anything visual. setIndex() will possibly switch 1137 // the models and broadcast the event. 1138 mLayout.uiSelectingTab(time, mStackTabs[clicked].getId()); 1139 } 1140 } 1141 } 1142 1143 /** 1144 * Tests if a point is inside the closing button of the tab. 1145 * 1146 * @param x The horizontal coordinate of the hit testing point. 1147 * @param y The vertical coordinate of the hit testing point. 1148 * @param layoutTab The {@link LayoutTab} to test on. 1149 * @return Whether the hit testing point is inside the tab. 1150 */ 1151 @VisibleForTesting checkCloseHitTestOnLayoutTab(float x, float y, LayoutTab layoutTab)1152 public boolean checkCloseHitTestOnLayoutTab(float x, float y, LayoutTab layoutTab) { 1153 RectF closeRectangle = getCloseBoundsOnLayoutTab(layoutTab); 1154 return closeRectangle != null ? closeRectangle.contains(x, y) : false; 1155 } 1156 1157 /** 1158 * @param layoutTab The {@link LayoutTab} to check. 1159 * @return The bounds of the {@link LayoutTab} of the close button. {@code null} if the close 1160 * button is not clickable. 1161 */ 1162 @VisibleForTesting getCloseBoundsOnLayoutTab(LayoutTab layoutTab)1163 public RectF getCloseBoundsOnLayoutTab(LayoutTab layoutTab) { 1164 if (!layoutTab.get(LayoutTab.IS_TITLE_NEEDED) || !layoutTab.get(LayoutTab.IS_VISIBLE) 1165 || layoutTab.get(LayoutTab.BORDER_CLOSE_BUTTON_ALPHA) < 0.5f 1166 || layoutTab.get(LayoutTab.BORDER_ALPHA) < 0.5f 1167 || layoutTab.get(LayoutTab.BORDER_ALPHA) != 1.0f 1168 || Math.abs(layoutTab.get(LayoutTab.TILT_X_IN_DEGREES)) > 1.0f 1169 || Math.abs(layoutTab.get(LayoutTab.TILT_Y_IN_DEGREES)) > 1.0f) { 1170 return null; 1171 } 1172 RectF closePlacement = layoutTab.get(LayoutTab.CLOSE_PLACEMENT); 1173 closePlacement.set(0, 0, LayoutTab.CLOSE_BUTTON_WIDTH_DP, LayoutTab.CLOSE_BUTTON_WIDTH_DP); 1174 if (layoutTab.get(LayoutTab.CLOSE_BUTTON_IS_ON_RIGHT)) { 1175 closePlacement.offset(layoutTab.getFinalContentWidth() - closePlacement.width(), 0.f); 1176 } 1177 if (closePlacement.bottom > layoutTab.getFinalContentHeight() 1178 || closePlacement.right > layoutTab.getFinalContentWidth()) { 1179 return null; 1180 } 1181 closePlacement.offset(layoutTab.get(LayoutTab.X) + layoutTab.get(LayoutTab.CLIPPED_X), 1182 layoutTab.get(LayoutTab.Y) + layoutTab.get(LayoutTab.CLIPPED_Y)); 1183 closePlacement.inset(-mCompositorButtonSlop, -mCompositorButtonSlop); 1184 1185 return closePlacement; 1186 } 1187 1188 /* 1189 * Initialization and Utility Methods 1190 */ 1191 1192 /** 1193 * @param context The current Android's context. 1194 */ contextChanged(Context context)1195 public void contextChanged(Context context) { 1196 Resources res = context.getResources(); 1197 final float pxToDp = 1.0f / res.getDisplayMetrics().density; 1198 1199 mMinScrollMotion = DRAG_MOTION_THRESHOLD_DP; 1200 final float maxOverScrollPx = res.getDimensionPixelOffset(R.dimen.over_scroll); 1201 final float maxUnderScrollPx = Math.round(maxOverScrollPx * MAX_UNDER_SCROLL_SCALE); 1202 mMaxOverScroll = maxOverScrollPx * pxToDp; 1203 mMaxUnderScroll = maxUnderScrollPx * pxToDp; 1204 mMaxOverScrollAngle = res.getInteger(R.integer.over_scroll_angle); 1205 mMaxOverScrollSlide = res.getDimensionPixelOffset(R.dimen.over_scroll_slide) * pxToDp; 1206 mBorderTransparentTop = 1207 res.getDimension(R.dimen.tabswitcher_border_frame_transparent_top) * pxToDp; 1208 mBorderTransparentSide = 1209 res.getDimension(R.dimen.tabswitcher_border_frame_transparent_side) * pxToDp; 1210 mBorderTopPadding = res.getDimension(R.dimen.tabswitcher_border_frame_padding_top) * pxToDp; 1211 mBorderLeftPadding = 1212 res.getDimension(R.dimen.tabswitcher_border_frame_padding_left) * pxToDp; 1213 mCompositorButtonSlop = res.getDimension(R.dimen.compositor_button_slop) * pxToDp; 1214 1215 // Just in case the density has changed, rebuild the OverScroller. 1216 mScroller = new StackScroller(context); 1217 } 1218 1219 /** 1220 * @param width The new width of the layout. 1221 * @param height The new height of the layout. 1222 * @param orientation The new orientation of the layout. 1223 */ notifySizeChanged(float width, float height, @Orientation int orientation)1224 public void notifySizeChanged(float width, float height, @Orientation int orientation) { 1225 updateCurrentMode(orientation); 1226 1227 // Changing the orientation can change which side of the tab we want to show the close 1228 // button on (if the horizontal tab switcher experiment is not enabled). 1229 if (mStackTabs == null) return; 1230 boolean closeButtonIsOnRight = isCloseButtonOnRight(); 1231 for (int i = 0; i < mStackTabs.length; i++) { 1232 mStackTabs[i].getLayoutTab().setCloseButtonIsOnRight(closeButtonIsOnRight); 1233 } 1234 } 1235 getScrollDimensionSize()1236 protected float getScrollDimensionSize() { 1237 return mCurrentMode == Orientation.PORTRAIT ? mLayout.getHeightMinusContentOffsetsDp() 1238 : mLayout.getWidth(); 1239 } 1240 1241 /** 1242 * Gets the tab instance at the requested position. 1243 * 1244 * @param x The x coordinate where to perform the hit test. 1245 * @param y The y coordinate where to perform the hit test. 1246 * @return The instance of the tab selected. null if none. 1247 */ getTabAtPositon(float x, float y)1248 private StackTab getTabAtPositon(float x, float y) { 1249 int tabIndexAtPosition = getTabIndexAtPositon(x, y, 0); 1250 return tabIndexAtPosition < 0 ? null : mStackTabs[tabIndexAtPosition]; 1251 } 1252 1253 /** 1254 * Gets the tab index at the requested position. 1255 * 1256 * @param x The x coordinate where to perform the hit test. 1257 * @param y The y coordinate where to perform the hit test. 1258 * @return The index of the tab selected. -1 if none. 1259 */ getTabIndexAtPositon(float x, float y)1260 protected int getTabIndexAtPositon(float x, float y) { 1261 return getTabIndexAtPositon(x, y, 0); 1262 } 1263 1264 /** 1265 * Gets the tab index at the requested position. 1266 * 1267 * @param x The x coordinate where to perform the hit test. 1268 * @param y The y coordinate where to perform the hit test. 1269 * @param slop The acceptable distance to a tab for it to be considered. 1270 * @return The index of the tab selected. -1 if none. 1271 */ getTabIndexAtPositon(float x, float y, float slop)1272 private int getTabIndexAtPositon(float x, float y, float slop) { 1273 int closestIndex = -1; 1274 float closestDistance = mLayout.getHeight() + mLayout.getWidth(); 1275 if (mStackTabs != null) { 1276 for (int i = mStackTabs.length - 1; i >= 0; --i) { 1277 // This is a fail safe. We should never have a situation where a dying 1278 // {@link LayoutTab} can get accessed (the animation check should catch it). 1279 if (!mStackTabs[i].isDying() && mStackTabs[i].getLayoutTab().isVisible()) { 1280 float d = computeDistanceToLayoutTab(x, y, mStackTabs[i].getLayoutTab()); 1281 // Strict '<' is very important here because we might have several tab at 1282 // the same place and we want the one above. 1283 if (d < closestDistance) { 1284 closestIndex = i; 1285 closestDistance = d; 1286 if (d == 0) break; 1287 } 1288 } 1289 } 1290 } 1291 return closestDistance <= slop ? closestIndex : -1; 1292 } 1293 1294 /** 1295 * Computes the Manhattan-ish distance to the edge of the tab. 1296 * This distance is good enough for click detection. 1297 * 1298 * @param x X coordinate of the hit testing point. 1299 * @param y Y coordinate of the hit testing point. 1300 * @param layoutTab The targeting tab. 1301 * @return The Manhattan-ish distance to the tab. 1302 */ computeDistanceToLayoutTab(float x, float y, LayoutTab layoutTab)1303 private static float computeDistanceToLayoutTab(float x, float y, LayoutTab layoutTab) { 1304 final RectF bounds = getClickTargetBoundsForLayoutTab(layoutTab); 1305 float dx = Math.max(bounds.left - x, x - bounds.right); 1306 float dy = Math.max(bounds.top - y, y - bounds.bottom); 1307 return Math.max(0.0f, Math.max(dx, dy)); 1308 } 1309 1310 /** 1311 * @return The rectangle that represents the click target of the tab. 1312 */ getClickTargetBoundsForLayoutTab(LayoutTab layoutTab)1313 private static RectF getClickTargetBoundsForLayoutTab(LayoutTab layoutTab) { 1314 final float borderScaled = BORDER_THICKNESS_DP * layoutTab.get(LayoutTab.BORDER_SCALE); 1315 RectF bounds = layoutTab.get(LayoutTab.BOUNDS); 1316 bounds.top = layoutTab.get(LayoutTab.Y) + layoutTab.get(LayoutTab.CLIPPED_Y) - borderScaled; 1317 bounds.bottom = layoutTab.get(LayoutTab.Y) + layoutTab.get(LayoutTab.CLIPPED_Y) 1318 + layoutTab.getFinalContentHeight() + borderScaled; 1319 bounds.left = 1320 layoutTab.get(LayoutTab.X) + layoutTab.get(LayoutTab.CLIPPED_X) - borderScaled; 1321 bounds.right = layoutTab.get(LayoutTab.X) + layoutTab.get(LayoutTab.CLIPPED_X) 1322 + layoutTab.getFinalContentWidth() + borderScaled; 1323 return bounds; 1324 } 1325 1326 /** 1327 * ComputeTabPosition pass 1: 1328 * Combine the overall stack scale with the animated tab scale. 1329 * 1330 * @param stackRect The frame of the stack. 1331 */ computeTabScaleAlphaDepthHelper(RectF stackRect)1332 private void computeTabScaleAlphaDepthHelper(RectF stackRect) { 1333 final float stackScale = getStackScale(stackRect); 1334 final float discardRange = getDiscardRange(); 1335 1336 for (int i = 0; i < mStackTabs.length; ++i) { 1337 assert mStackTabs[i] != null; 1338 StackTab stackTab = mStackTabs[i]; 1339 LayoutTab layoutTab = stackTab.getLayoutTab(); 1340 final float discard = stackTab.getDiscardAmount(); 1341 1342 // Scale 1343 float discardScale = 1344 computeDiscardScale(discard, discardRange, stackTab.getDiscardFromClick()); 1345 layoutTab.setScale(stackTab.getScale() * discardScale * stackScale); 1346 layoutTab.setBorderScale(discardScale); 1347 1348 // Alpha 1349 float discardAlpha = computeDiscardAlpha(discard, discardRange); 1350 layoutTab.setAlpha(stackTab.getAlpha() * discardAlpha); 1351 } 1352 } 1353 1354 /** 1355 * ComputeTabPosition pass 2: 1356 * Adjust the scroll offsets of each tab so no there is no void in between tabs. 1357 */ computeTabScrollOffsetHelper()1358 private void computeTabScrollOffsetHelper() { 1359 float maxScrollOffset = Float.MAX_VALUE; 1360 for (int i = 0; i < mStackTabs.length; ++i) { 1361 if (mStackTabs[i].isDying()) continue; 1362 1363 float tabScrollOffset = Math.min(maxScrollOffset, mStackTabs[i].getScrollOffset()); 1364 mStackTabs[i].setScrollOffset(tabScrollOffset); 1365 1366 float maxScreenScrollOffset = scrollToScreen(mScrollOffset + tabScrollOffset); 1367 maxScrollOffset = -mScrollOffset 1368 + screenToScroll(maxScreenScrollOffset 1369 + mStackTabs[i].getSizeInScrollDirection(mCurrentMode)); 1370 } 1371 } 1372 1373 /** 1374 * @return Whether or not to enable logic that gives the tabs a "stacked" appearance at the 1375 * top (in portrait mode) or left (in landscape mode). 1376 */ shouldStackTabsAtTop()1377 protected abstract boolean shouldStackTabsAtTop(); 1378 1379 /** 1380 * @return Whether or not to enable logic that gives the tabs a "stacked" appearance at the 1381 * bottom (in portrait mode) or right (in landscape mode). 1382 */ shouldStackTabsAtBottom()1383 protected abstract boolean shouldStackTabsAtBottom(); 1384 1385 /** 1386 * @return How much the stack should adjust the y position of each LayoutTab in portrait 1387 * mode (as a fraction of the amount space that would be above and below the tab if 1388 * it were centered). 1389 */ getStackPortraitYOffsetProportion()1390 protected abstract float getStackPortraitYOffsetProportion(); 1391 1392 /** 1393 * @return How much the stack should adjust the x position of each LayoutTab in landscape 1394 * mode (as a fraction of the amount space that would be to the left and right of 1395 * the tab if it were centered). 1396 */ getStackLandscapeStartOffsetProportion()1397 protected abstract float getStackLandscapeStartOffsetProportion(); 1398 1399 /** 1400 * @return How much the stack should adjust the x position of each LayoutTab in portrait 1401 * mode (as a fraction of the amount space that would be above and below the tab if 1402 * it were centered). 1403 */ getStackLandscapeYOffsetProportion()1404 protected abstract float getStackLandscapeYOffsetProportion(); 1405 1406 /** 1407 * ComputeTabPosition pass 3: 1408 * Compute the position of the tabs. Adjust for top and bottom stacking. 1409 * 1410 * @param stackRect The frame of the stack. 1411 */ computeTabOffsetHelper(RectF stackRect)1412 private void computeTabOffsetHelper(RectF stackRect) { 1413 final boolean portrait = mCurrentMode == Orientation.PORTRAIT; 1414 1415 // Precompute the position using scroll offset and top stacking. 1416 final float parentWidth = stackRect.width(); 1417 final float parentHeight = stackRect.height(); 1418 final float overscrollPercent = computeOverscrollPercent(); 1419 final float scrollOffset = 1420 MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)); 1421 final float stackScale = getStackScale(stackRect); 1422 1423 int stackedCount = 0; 1424 float minStackedPosition = 0.0f; 1425 for (int i = 0; i < mStackTabs.length; ++i) { 1426 assert mStackTabs[i] != null; 1427 StackTab stackTab = mStackTabs[i]; 1428 LayoutTab layoutTab = stackTab.getLayoutTab(); 1429 1430 // Position 1431 final float stackScrollOffset = 1432 stackTab.isDying() ? mScrollOffsetForDyingTabs : scrollOffset; 1433 float screenScrollOffset = approxScreen(stackTab, stackScrollOffset); 1434 1435 if (shouldStackTabsAtTop()) { 1436 // Resolve top stacking 1437 screenScrollOffset = Math.max(minStackedPosition, screenScrollOffset); 1438 if (stackedCount < MAX_NUMBER_OF_STACKED_TABS_TOP) { 1439 // This make sure all the tab get stacked up as one when all the tabs do a 1440 // full roll animation. 1441 final float tiltXcos = (float) Math.cos(Math.toRadians(layoutTab.getTiltX())); 1442 final float tiltYcos = (float) Math.cos(Math.toRadians(layoutTab.getTiltY())); 1443 float collapse = Math.min(Math.abs(tiltXcos), Math.abs(tiltYcos)); 1444 collapse *= layoutTab.getAlpha(); 1445 minStackedPosition += StackTab.sStackedTabVisibleSize * collapse; 1446 } 1447 stackedCount += stackTab.isDying() ? 0 : 1; 1448 if (overscrollPercent < 0) { 1449 // Oversroll at the top of the screen. For the first 1450 // OVERSCROLL_TOP_SLIDE_PCTG of the overscroll, slide the tabs 1451 // together so they completely overlap. After that, stop scrolling the 1452 // tabs. 1453 screenScrollOffset += 1454 (overscrollPercent / OVERSCROLL_TOP_SLIDE_PCTG) * screenScrollOffset; 1455 screenScrollOffset = Math.max(0, screenScrollOffset); 1456 } 1457 } 1458 1459 // Note: All the Offsets except for centering shouldn't depend on the tab's scaling 1460 // because it interferes the scaling center. 1461 1462 // Centers the tab in its parent. 1463 float xIn = (parentWidth - layoutTab.getScaledContentWidth()) / 2.0f; 1464 float yIn = (parentHeight - layoutTab.getScaledContentHeight()) / 2.0f; 1465 1466 // We want slight offset from the center so that multiple tab browsing 1467 // have more space to its expanding direction. e.g., On portrait mode, 1468 // there will be more space on the bottom than top. 1469 final float horizontalPadding = 1470 (parentWidth 1471 - layoutTab.getOriginalContentWidth() * getScaleAmount() * stackScale) 1472 / 2.0f; 1473 final float verticalPadding = 1474 (parentHeight 1475 - layoutTab.getOriginalContentHeight() * getScaleAmount() * stackScale) 1476 / 2.0f; 1477 1478 if (portrait) { 1479 yIn += getStackPortraitYOffsetProportion() * verticalPadding; 1480 yIn += screenScrollOffset; 1481 } else { 1482 if (LocalizationUtils.isLayoutRtl()) { 1483 xIn -= getStackLandscapeStartOffsetProportion() * horizontalPadding; 1484 xIn -= screenScrollOffset; 1485 } else { 1486 xIn += getStackLandscapeStartOffsetProportion() * horizontalPadding; 1487 xIn += screenScrollOffset; 1488 } 1489 yIn += getStackLandscapeYOffsetProportion() * verticalPadding; 1490 } 1491 1492 layoutTab.setX(xIn); 1493 layoutTab.setY(yIn); 1494 } 1495 1496 if (shouldStackTabsAtBottom()) { 1497 // Resolve bottom stacking 1498 stackedCount = 0; 1499 float maxStackedPosition = 1500 portrait ? mLayout.getHeightMinusContentOffsetsDp() : mLayout.getWidth(); 1501 for (int i = mStackTabs.length - 1; i >= 0; i--) { 1502 assert mStackTabs[i] != null; 1503 StackTab stackTab = mStackTabs[i]; 1504 LayoutTab layoutTab = stackTab.getLayoutTab(); 1505 if (stackTab.isDying()) continue; 1506 1507 float pos; 1508 if (portrait) { 1509 pos = layoutTab.getY(); 1510 layoutTab.setY(Math.min(pos, maxStackedPosition)); 1511 } else if (LocalizationUtils.isLayoutRtl()) { 1512 // On RTL landscape, pos is a distance between tab's right and mLayout's 1513 // right. 1514 float posOffset = mLayout.getWidth() 1515 - layoutTab.getOriginalContentWidth() * getScaleAmount() * stackScale; 1516 pos = -layoutTab.getX() + posOffset; 1517 layoutTab.setX(-Math.min(pos, maxStackedPosition) + posOffset); 1518 } else { 1519 pos = layoutTab.getX(); 1520 layoutTab.setX(Math.min(pos, maxStackedPosition)); 1521 } 1522 if (pos >= maxStackedPosition && stackedCount < MAX_NUMBER_OF_STACKED_TABS_BOTTOM) { 1523 maxStackedPosition -= StackTab.sStackedTabVisibleSize; 1524 stackedCount++; 1525 } 1526 } 1527 } 1528 1529 // final position blend 1530 final float discardRange = getDiscardRange(); 1531 for (int i = 0; i < mStackTabs.length; ++i) { 1532 assert mStackTabs[i] != null; 1533 StackTab stackTab = mStackTabs[i]; 1534 LayoutTab layoutTab = stackTab.getLayoutTab(); 1535 1536 final float xIn = layoutTab.getX() + stackTab.getXInStackOffset(); 1537 final float yIn = layoutTab.getY() + stackTab.getYInStackOffset(); 1538 final float xOut = stackTab.getXOutOfStack(); 1539 final float yOut = stackTab.getYOutOfStack(); 1540 float x = MathUtils.interpolate(xOut, xIn, stackTab.getXInStackInfluence()); 1541 float y = MathUtils.interpolate(yOut, yIn, stackTab.getYInStackInfluence()); 1542 1543 // Discard offsets 1544 if (stackTab.getDiscardAmount() != 0) { 1545 float discard = stackTab.getDiscardAmount(); 1546 boolean fromClick = stackTab.getDiscardFromClick(); 1547 float scale = computeDiscardScale(discard, discardRange, fromClick); 1548 float deltaX = stackTab.getDiscardOriginX() 1549 - stackTab.getLayoutTab().getOriginalContentWidth() / 2.f; 1550 float deltaY = stackTab.getDiscardOriginY() 1551 - stackTab.getLayoutTab().getOriginalContentHeight() / 2.f; 1552 float discardOffset = fromClick ? 0.f : discard; 1553 if (portrait) { 1554 x += discardOffset + deltaX * (1.f - scale); 1555 y += deltaY * (1.f - scale); 1556 } else { 1557 x += deltaX * (1.f - scale); 1558 y += discardOffset + deltaY * (1.f - scale); 1559 } 1560 } 1561 1562 // Finally apply the stack translation 1563 layoutTab.setX(stackRect.left + x); 1564 layoutTab.setY(stackRect.top + y); 1565 } 1566 } 1567 1568 /** 1569 * ComputeTabPosition pass 5: 1570 * Computes the clipping, visibility and adjust overall alpha if needed. 1571 */ computeTabClippingVisibilityHelper()1572 protected abstract void computeTabClippingVisibilityHelper(); 1573 1574 /** 1575 * Computes the index that should be assumed to be the currently centered tab, for purposes 1576 * of prioritizing which thumbnails to render. 1577 */ computeReferenceIndex()1578 protected abstract int computeReferenceIndex(); 1579 1580 /** 1581 * ComputeTabPosition pass 6: 1582 * Updates the visibility sorting value to use to figure out which thumbnails to load. 1583 * 1584 * @param stackRect The frame of the stack. 1585 */ computeTabVisibilitySortingHelper(RectF stackRect)1586 private void computeTabVisibilitySortingHelper(RectF stackRect) { 1587 int referenceIndex = mReferenceOrderIndex; 1588 if (referenceIndex == -1) referenceIndex = computeReferenceIndex(); 1589 1590 final float width = mLayout.getWidth(); 1591 final float height = mLayout.getHeight(); 1592 final float left = MathUtils.clamp(stackRect.left, 0, width); 1593 final float right = MathUtils.clamp(stackRect.right, 0, width); 1594 final float top = MathUtils.clamp(stackRect.top, 0, height); 1595 final float bottom = MathUtils.clamp(stackRect.bottom, 0, height); 1596 final float stackArea = (right - left) * (bottom - top); 1597 final float layoutArea = Math.max(width * height, 1.0f); 1598 final float stackVisibilityMultiplier = stackArea / layoutArea; 1599 1600 for (int i = 0; i < mStackTabs.length; i++) { 1601 mStackTabs[i].updateStackVisiblityValue(stackVisibilityMultiplier); 1602 mStackTabs[i].updateVisiblityValue(referenceIndex); 1603 } 1604 } 1605 1606 /** 1607 * Determine the current amount of overscroll. If the value is 0, there is 1608 * no overscroll. If the value is < 0, tabs are overscrolling towards the 1609 * top or or left. If the value is > 0, tabs are overscrolling towards the 1610 * bottom or right. 1611 */ computeOverscrollPercent()1612 private float computeOverscrollPercent() { 1613 if (mOverScrollOffset >= 0) { 1614 return mOverScrollOffset / mMaxOverScroll; 1615 } else { 1616 return mOverScrollOffset / mMaxUnderScroll; 1617 } 1618 } 1619 1620 /** 1621 * Update the tilt of each tab for full roll if necessary. 1622 * 1623 * @param time The current time of the app in ms. 1624 * @param stackRect The frame of the stack. 1625 */ fullRollHelper(long time, RectF stackRect)1626 private void fullRollHelper(long time, RectF stackRect) { 1627 if (mOverviewAnimationType != OverviewAnimationType.FULL_ROLL 1628 && computeOverscrollPercent() < 0 1629 && mOverScrollCounter >= OVERSCROLL_FULL_ROLL_TRIGGER) { 1630 startAnimation(time, OverviewAnimationType.FULL_ROLL); 1631 mOverScrollCounter = 0; 1632 // Remove overscroll so when the animation finishes the overscroll won't 1633 // be bothering. 1634 setScrollTarget( 1635 MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)), 1636 false); 1637 } 1638 } 1639 1640 /** Whether or not to apply logic to enforce that there are no gaps between tabs. */ shouldCloseGapsBetweenTabs()1641 protected abstract boolean shouldCloseGapsBetweenTabs(); 1642 1643 /** 1644 * Computes the {@link LayoutTab} position from the stack and the stackTab data. 1645 * 1646 * @param time The current time of the app in ms. 1647 * @param stackRect The rectangle the stack should be drawn into. It may change over frames. 1648 */ computeTabPosition(long time, RectF stackRect)1649 public void computeTabPosition(long time, RectF stackRect) { 1650 if (mStackTabs == null || mStackTabs.length == 0) return; 1651 1652 // Step 1: Updates the {@link LayoutTab} scale, alpha and depth values. 1653 computeTabScaleAlphaDepthHelper(stackRect); 1654 1655 if (shouldCloseGapsBetweenTabs()) { 1656 // Step 2: Fix tab scroll offsets to avoid gaps. 1657 computeTabScrollOffsetHelper(); 1658 } 1659 1660 // Step 3: Compute the actual position. 1661 computeTabOffsetHelper(stackRect); 1662 1663 // Step 4: Test if the full-roll animation needs to be run. 1664 fullRollHelper(time, stackRect); 1665 1666 // Step 5: Clipping, visibility and adjust overall alpha. 1667 computeTabClippingVisibilityHelper(); 1668 1669 // Step 6: Update visibility sorting for prioritizing thumbnail texture request. 1670 computeTabVisibilitySortingHelper(stackRect); 1671 } 1672 1673 /** 1674 * @param stackFocus The current amount of focus of the stack [0 .. 1] 1675 * @param orderIndex The index in the stack of the focused tab. -1 to ask the 1676 * stack to compute it. 1677 */ setStackFocusInfo(float stackFocus, int orderIndex)1678 public void setStackFocusInfo(float stackFocus, int orderIndex) { 1679 if (mStackTabs == null) return; 1680 mReferenceOrderIndex = orderIndex; 1681 for (int i = 0; i < mStackTabs.length; i++) { 1682 mStackTabs[i].getLayoutTab().setBorderCloseButtonAlpha(stackFocus); 1683 } 1684 } 1685 1686 /** 1687 * Reverts the closure of the tab specified by {@code tabId}. This will run an undiscard 1688 * animation on that tab. 1689 * @param time The current time of the app in ms. 1690 * @param tabId The id of the tab to animate. 1691 */ undoClosure(long time, int tabId)1692 public void undoClosure(long time, int tabId) { 1693 createStackTabs(true); 1694 if (mStackTabs == null) return; 1695 1696 for (int i = 0; i < mStackTabs.length; i++) { 1697 StackTab tab = mStackTabs[i]; 1698 1699 if (tab.getId() == tabId) { 1700 tab.setDiscardAmount(getDiscardRange()); 1701 tab.setDying(false); 1702 tab.getLayoutTab().setMaxContentHeight(getMaxTabHeight()); 1703 } 1704 } 1705 1706 mSpacing = computeSpacing(mStackTabs.length); 1707 startAnimation(time, OverviewAnimationType.UNDISCARD); 1708 } 1709 1710 /** 1711 * Creates the {@link StackTab}s needed for display and populates {@link #mStackTabs}. 1712 * It is called from show() at the beginning of every new draw phase. It tries to reuse old 1713 * {@link StackTab} instead of creating new ones every time. 1714 * @param restoreState Whether or not to restore the {@link LayoutTab} state when we rebuild 1715 * the {@link StackTab}s. There are some properties like maximum content 1716 * size or whether or not to show the toolbar that might have to be 1717 * restored if we're calling this while the switcher is already visible. 1718 */ createStackTabs(boolean restoreState)1719 private void createStackTabs(boolean restoreState) { 1720 if (mTabList == null) return; 1721 1722 final int count = mTabList.getCount(); 1723 if (count == 0) { 1724 cleanupTabs(); 1725 } else { 1726 StackTab[] oldTabs = mStackTabs; 1727 mStackTabs = new StackTab[count]; 1728 1729 final boolean isIncognito = mTabList.isIncognito(); 1730 final boolean needTitle = !mLayout.isStartingToHide(); 1731 for (int i = 0; i < count; ++i) { 1732 Tab tab = mTabList.getTabAt(i); 1733 int tabId = tab != null ? tab.getId() : Tab.INVALID_TAB_ID; 1734 mStackTabs[i] = findTabById(oldTabs, tabId); 1735 1736 float maxContentWidth = -1.f; 1737 float maxContentHeight = -1.f; 1738 1739 if (mStackTabs[i] != null && mStackTabs[i].getLayoutTab() != null && restoreState) { 1740 maxContentWidth = mStackTabs[i].getLayoutTab().getMaxContentWidth(); 1741 maxContentHeight = mStackTabs[i].getLayoutTab().getMaxContentHeight(); 1742 } 1743 1744 LayoutTab layoutTab = mLayout.createLayoutTab(tabId, isIncognito, 1745 Layout.SHOW_CLOSE_BUTTON, needTitle, maxContentWidth, maxContentHeight); 1746 layoutTab.setInsetBorderVertical(true); 1747 layoutTab.setShowToolbar(true); 1748 layoutTab.setToolbarAlpha(0.f); 1749 layoutTab.setAnonymizeToolbar(!mIsStackForCurrentTabList || mTabList.index() != i); 1750 layoutTab.setCloseButtonIsOnRight(isCloseButtonOnRight()); 1751 1752 if (mStackTabs[i] == null) { 1753 mStackTabs[i] = new StackTab(layoutTab); 1754 } else { 1755 mStackTabs[i].setLayoutTab(layoutTab); 1756 } 1757 1758 mStackTabs[i].setNewIndex(i); 1759 // The initial enterStack animation will take care of 1760 // positioning, scaling, etc. 1761 } 1762 } 1763 } 1764 findTabById(StackTab[] layoutTabs, int id)1765 private StackTab findTabById(StackTab[] layoutTabs, int id) { 1766 if (layoutTabs == null) return null; 1767 final int count = layoutTabs.length; 1768 for (int i = 0; i < count; i++) { 1769 if (layoutTabs[i].getId() == id) return layoutTabs[i]; 1770 } 1771 return null; 1772 } 1773 1774 /** 1775 * Creates a {@link StackTab}. 1776 * This function should ONLY be called from {@link #tabCreated(long, int)} and nowhere else. 1777 * 1778 * @param id The id of the tab. 1779 * @return Whether the tab has successfully been created and added. 1780 */ createTabHelper(int id)1781 private boolean createTabHelper(int id) { 1782 if (TabModelUtils.getTabById(mTabList, id) == null) return false; 1783 1784 // Check to see if the tab already exists in our model. This is 1785 // just to cover the case where stackEntered and then tabCreated() 1786 // called in a row. 1787 if (mStackTabs != null) { 1788 final int count = mStackTabs.length; 1789 for (int i = 0; i < count; ++i) { 1790 if (mStackTabs[i].getId() == id) { 1791 return false; 1792 } 1793 } 1794 } 1795 1796 createStackTabs(true); 1797 1798 return true; 1799 } 1800 1801 /** 1802 * @return The percentage of the screen that defines the spacing between tabs by default (no 1803 * pinch). 1804 */ getSpacingScreen()1805 protected abstract float getSpacingScreen(); 1806 1807 /** 1808 * This redetermines the proper spacing for the {@link StackTab}. It takes in a parameter 1809 * for the size instead of using the mStackTabs.length property because we could be setting 1810 * the spacing for a delete before the tab has been removed (will help with animations). 1811 * @param layoutTabCount The number of layout tabs currently in the Stack. 1812 * @return How far apart the tabs should be spaced (modulo certain 1813 * adjustments, such as non-linear warping). 1814 */ computeSpacing(int layoutTabCount)1815 protected abstract int computeSpacing(int layoutTabCount); 1816 getStackScale(RectF stackRect)1817 private float getStackScale(RectF stackRect) { 1818 return mCurrentMode == Orientation.PORTRAIT 1819 ? stackRect.width() / mLayout.getWidth() 1820 : stackRect.height() / mLayout.getHeightMinusContentOffsetsDp(); 1821 } 1822 setScrollTarget(float offset, boolean immediate)1823 protected void setScrollTarget(float offset, boolean immediate) { 1824 // Ensure that the stack cannot be scrolled too far in either direction. 1825 // mScrollOffset is clamped between [-min, 0], where offset 0 has the 1826 // farthest back tab (the first tab) at the top, with everything else 1827 // pulled down, and -min has the tab at the top of the stack (the last 1828 // tab) is pulled up and fully visible. 1829 final boolean overscroll = allowOverscroll(); 1830 mScrollTarget = MathUtils.clamp(offset, getMinScroll(overscroll), getMaxScroll(overscroll)); 1831 if (immediate) mScrollOffset = mScrollTarget; 1832 mCurrentScrollDirection = Math.signum(mScrollTarget - mScrollOffset); 1833 } 1834 1835 /** 1836 * Gets the min scroll value. 1837 * 1838 * @param allowUnderScroll True if underscroll is allowed. 1839 */ getMinScroll(boolean allowUnderScroll)1840 protected abstract float getMinScroll(boolean allowUnderScroll); 1841 1842 /** 1843 * Gets the max scroll value. 1844 * 1845 * @param allowOverscroll True if overscroll is allowed. 1846 */ getMaxScroll(boolean allowOverscroll)1847 protected float getMaxScroll(boolean allowOverscroll) { 1848 if (mStackTabs == null || !allowOverscroll) { 1849 return 0; 1850 } else { 1851 return mMaxOverScroll; 1852 } 1853 } 1854 stopScrollingMovement(long time)1855 private void stopScrollingMovement(long time) { 1856 // We have to cancel the fling if it is in progress. 1857 if (mScroller.computeScrollOffset(time)) { 1858 // Set the current offset and target to the current scroll 1859 // position so the {@link StackTab}s won't scroll anymore. 1860 setScrollTarget(mScroller.getCurrY(), true /* immediate */); 1861 1862 // Tell the scroller to finish scrolling. 1863 mScroller.forceFinished(true); 1864 } else { 1865 // If we aren't scrolling just set the target to the current 1866 // offset so we don't move anymore. 1867 setScrollTarget(mScrollOffset, false); 1868 } 1869 } 1870 allowOverscroll()1871 protected boolean allowOverscroll() { 1872 // All the animations that want to leave the tilt value to be set by the overscroll must 1873 // be added here. 1874 return mOverviewAnimationType == OverviewAnimationType.NONE 1875 || mOverviewAnimationType == OverviewAnimationType.VIEW_MORE 1876 || mOverviewAnimationType == OverviewAnimationType.ENTER_STACK; 1877 } 1878 1879 /** 1880 * Smoothes input signal. The definition of the input is lower than the 1881 * pixel density of the screen so we need to smooth the input to give the illusion of smooth 1882 * animation on screen from chunky inputs. 1883 * The combination of 20 pixels and 0.9f ensures that the output is not more than 2 pixels 1884 * away from the target. 1885 * TODO: This has nothing to do with time, just draw rate. 1886 * Is this okay or do we want to have the interpolation based on the time elapsed? 1887 * @param current The current value of the signal. 1888 * @param input The raw input value. 1889 * @return The smoothed signal. 1890 */ smoothInput(float current, float input)1891 private float smoothInput(float current, float input) { 1892 current = MathUtils.clamp(current, input - 20, input + 20); 1893 return MathUtils.interpolate(current, input, 0.9f); 1894 } 1895 forceScrollStop()1896 protected void forceScrollStop() { 1897 mScroller.forceFinished(true); 1898 updateOverscrollOffset(); 1899 mScrollTarget = mScrollOffset; 1900 } 1901 updateScrollOffset(long time)1902 private void updateScrollOffset(long time) { 1903 // If we are still scrolling, which is determined by a disparity 1904 // between our scroll offset and our scroll target, we need 1905 // to try to move closer to that position. 1906 if (mScrollOffset != mScrollTarget) { 1907 if (mScroller.computeScrollOffset(time)) { 1908 final float newScrollOffset = mScroller.getCurrY(); 1909 evenOutTabs(newScrollOffset - mScrollOffset, true); 1910 // We are currently in the process of being flinged. Just 1911 // ask the scroller for the new position. 1912 mScrollOffset = newScrollOffset; 1913 } else { 1914 // We are just being dragged or scrolled, not flinged. This 1915 // means we should move closer to our target quickly but not 1916 // quickly enough to show the stuttering that could be 1917 // exposed by the touch event rate. 1918 mScrollOffset = smoothInput(mScrollOffset, mScrollTarget); 1919 } 1920 mLayout.requestUpdate(); 1921 } else { 1922 // Make sure that the scroller is marked as finished when the destination is 1923 // reached. 1924 mScroller.forceFinished(true); 1925 } 1926 updateOverscrollOffset(); 1927 } 1928 updateOverscrollOffset()1929 private void updateOverscrollOffset() { 1930 float clamped = MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)); 1931 if (!allowOverscroll()) { 1932 mScrollOffset = clamped; 1933 } 1934 float overscroll = mScrollOffset - clamped; 1935 1936 // Counts the number of overscroll push in the same direction in a row. 1937 int derivativeState = (int) Math.signum(Math.abs(mOverScrollOffset) - Math.abs(overscroll)); 1938 if (derivativeState != mOverScrollDerivative && derivativeState == 1 && overscroll < 0) { 1939 mOverScrollCounter++; 1940 } else if (overscroll > 0 || mCurrentMode == Orientation.LANDSCAPE) { 1941 mOverScrollCounter = 0; 1942 } 1943 mOverScrollDerivative = derivativeState; 1944 1945 mOverScrollOffset = overscroll; 1946 } 1947 1948 /** 1949 * Called when the stack is opened to reset all the tab and scroll positions. 1950 */ resetAllScrollOffset()1951 protected abstract void resetAllScrollOffset(); 1952 approxScreen(StackTab tab, float globalScrollOffset)1953 protected float approxScreen(StackTab tab, float globalScrollOffset) { 1954 return scrollToScreen(tab.getScrollOffset() + globalScrollOffset); 1955 } 1956 1957 /** 1958 * Maps from scroll coordinates to screen coordinates. 1959 * @param scrollSpace The offset in scroll space. 1960 * @return The offset on screen corresponding to the scroll space offset. 1961 */ scrollToScreen(float scrollSpace)1962 public abstract float scrollToScreen(float scrollSpace); 1963 1964 /** 1965 * Maps from screen coordinates to scroll coordinates. This allows Stack subclasses (e.g. 1966 * OverlappingStack) to use non-linear scrolling. 1967 * @param scrollSpace The offset in screen space. 1968 * @return The offset in scroll space corresponding to the offset on screen. 1969 */ screenToScroll(float screenSpace)1970 public abstract float screenToScroll(float screenSpace); 1971 1972 /** 1973 * @return The range of the discard action. At the end of the +/- range the discarded tab 1974 * will be fully transparent. 1975 */ getDiscardRange()1976 private float getDiscardRange() { 1977 return getRange(DISCARD_RANGE_SCREEN); 1978 } 1979 getRange(float range)1980 private float getRange(float range) { 1981 return range 1982 * (mCurrentMode == Orientation.PORTRAIT ? mLayout.getWidth() 1983 : mLayout.getHeightMinusContentOffsetsDp()); 1984 } 1985 1986 /** 1987 * @return The maximum height of a layout tab in the tab switcher. 1988 */ getMaxTabHeight()1989 public abstract float getMaxTabHeight(); 1990 1991 /** 1992 * @return The current spacing between tabs. 1993 */ getSpacing()1994 public float getSpacing() { 1995 return mSpacing; 1996 } 1997 1998 /** 1999 * @return The current overall scroll offset for the Stack. 2000 */ getScrollOffset()2001 public float getScrollOffset() { 2002 return mScrollOffset; 2003 } 2004 2005 /** 2006 * Computes the scale of the tab based on its discard status. 2007 * 2008 * @param amount The discard amount. 2009 * @param range The range of the absolute value of discard amount. 2010 * @param fromClick Whether or not the discard was from a click or a swipe. 2011 * @return The scale of the tab to use to draw the tab. 2012 */ computeDiscardScale(float amount, float range, boolean fromClick)2013 public static float computeDiscardScale(float amount, float range, boolean fromClick) { 2014 if (Math.abs(amount) < 1.0f) return 1.0f; 2015 float t = amount / range; 2016 float endScale = fromClick ? DISCARD_END_SCALE_CLICK : DISCARD_END_SCALE_SWIPE; 2017 return MathUtils.interpolate(1.0f, endScale, Math.abs(t)); 2018 } 2019 2020 /** 2021 * Computes the alpha value of the tab based on its discard status. 2022 * 2023 * @param amount The discard amount. 2024 * @param range The range of the absolute value of discard amount. 2025 * @return The alpha value that need to be applied on the tab. 2026 */ computeDiscardAlpha(float amount, float range)2027 public static float computeDiscardAlpha(float amount, float range) { 2028 if (Math.abs(amount) < 1.0f) return 1.0f; 2029 float t = amount / range; 2030 t = MathUtils.clamp(t, -1.0f, 1.0f); 2031 return 1.f - Math.abs(t); 2032 } 2033 updateCurrentMode(@rientation int orientation)2034 protected void updateCurrentMode(@Orientation int orientation) { 2035 if (CachedFeatureFlags.isEnabled(ChromeFeatureList.HORIZONTAL_TAB_SWITCHER_ANDROID)) { 2036 mCurrentMode = Orientation.LANDSCAPE; 2037 } else { 2038 mCurrentMode = orientation; 2039 } 2040 2041 mDiscardDirection = getDefaultDiscardDirection(); 2042 final float opaqueTopPadding = mBorderTopPadding - mBorderTransparentTop; 2043 mAnimationFactory = new StackAnimation(this, mLayout.getWidth(), mLayout.getHeight(), 2044 mLayout.getTopContentOffsetDp(), mBorderTopPadding, opaqueTopPadding, 2045 mBorderLeftPadding, mCurrentMode); 2046 mViewAnimationFactory = new StackViewAnimation(mLayout.getContext().getResources()); 2047 if (mStackTabs == null) return; 2048 float width = mLayout.getWidth(); 2049 for (int i = 0; i < mStackTabs.length; i++) { 2050 LayoutTab tab = mStackTabs[i].getLayoutTab(); 2051 if (tab == null) continue; 2052 tab.setMaxContentWidth(width); 2053 tab.setMaxContentHeight(getMaxTabHeight()); 2054 } 2055 } 2056 2057 /** 2058 * Called to release everything. Called well after the view has been really hidden. 2059 */ cleanupTabs()2060 public void cleanupTabs() { 2061 mStackTabs = null; 2062 resetInputActionIndices(); 2063 } 2064 2065 /** 2066 * Resets all the indices that are pointing to tabs for various features. 2067 */ resetInputActionIndices()2068 protected void resetInputActionIndices() { 2069 mScrollingTab = null; 2070 mDiscardingTab = null; 2071 } 2072 2073 /** 2074 * Reset session based parameters. 2075 * Called before the a session starts. Before the show, regardless if the stack is 2076 * displayable. 2077 */ reset()2078 public void reset() { 2079 mIsDying = false; 2080 } 2081 2082 /** 2083 * Called when the swipe animation get initiated. It gives a chance to initialize 2084 * everything. 2085 * @param time The current time of the app in ms. 2086 * @param direction The direction the swipe is in. 2087 * @param x The horizontal coordinate the swipe started at in dp. 2088 * @param y The vertical coordinate the swipe started at in dp. 2089 */ swipeStarted(long time, @ScrollDirection int direction, float x, float y)2090 public void swipeStarted(long time, @ScrollDirection int direction, float x, float y) { 2091 if (direction != ScrollDirection.DOWN) return; 2092 2093 // Restart the enter stack animation with the new warp values. 2094 startAnimation(time, OverviewAnimationType.ENTER_STACK); 2095 2096 // Update the scroll offset to put the focused tab at the top. 2097 final int index = mTabList.index(); 2098 2099 if (mCurrentMode == Orientation.PORTRAIT 2100 || ChromeFeatureList.isEnabled(ChromeFeatureList.HORIZONTAL_TAB_SWITCHER_ANDROID)) { 2101 mScrollOffset = -index * mSpacing; 2102 } else { 2103 mScrollOffset = -index * mSpacing + x - LANDSCAPE_SWIPE_DRAG_TAB_OFFSET_DP; 2104 mScrollOffset = 2105 MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)); 2106 } 2107 setScrollTarget(mScrollOffset, true); 2108 2109 // Set up the tracking scroll parameters. 2110 mSwipeUnboundScrollOffset = mScrollOffset; 2111 mSwipeBoundedScrollOffset = mScrollOffset; 2112 2113 // Reset other state. 2114 mSwipeIsCancelable = false; 2115 mSwipeCanScroll = false; 2116 mInSwipe = true; 2117 } 2118 2119 /** 2120 * Updates a swipe gesture. 2121 * @param time The current time of the app in ms. 2122 * @param x The horizontal coordinate the swipe is currently at in dp. 2123 * @param y The vertical coordinate the swipe is currently at in dp. 2124 * @param dx The horizontal delta since the last update in dp. 2125 * @param dy The vertical delta since the last update in dp. 2126 * @param tx The horizontal difference between the start and the current position in dp. 2127 * @param ty The vertical difference between the start and the current position in dp. 2128 */ swipeUpdated(long time, float x, float y, float dx, float dy, float tx, float ty)2129 public void swipeUpdated(long time, float x, float y, float dx, float dy, float tx, float ty) { 2130 if (!mInSwipe) return; 2131 2132 final float toolbarSize = mLayout.getTopContentOffsetDp(); 2133 if (ty > toolbarSize) mSwipeCanScroll = true; 2134 if (!mSwipeCanScroll) return; 2135 2136 final int index = mTabList.index(); 2137 2138 // Check to make sure the index is still valid. 2139 if (index < 0 || index >= mStackTabs.length) { 2140 assert false : "Tab index out of bounds in Stack#swipeUpdated()"; 2141 return; 2142 } 2143 2144 final float delta = mCurrentMode == Orientation.PORTRAIT ? dy : dx; 2145 2146 // Update the unbound scroll offset, tracking delta regardless of constraints. 2147 mSwipeUnboundScrollOffset += delta; 2148 2149 // Figure out the new constrained position. 2150 final float minScroll = getMinScroll(true); 2151 final float maxScroll = getMaxScroll(true); 2152 float offset = MathUtils.clamp(mSwipeUnboundScrollOffset, minScroll, maxScroll); 2153 2154 final float constrainedDelta = offset - mSwipeBoundedScrollOffset; 2155 mSwipeBoundedScrollOffset = offset; 2156 2157 if (constrainedDelta == 0.f) return; 2158 2159 if (mCurrentMode == Orientation.PORTRAIT) { 2160 dy = constrainedDelta; 2161 } else { 2162 dx = constrainedDelta; 2163 } 2164 2165 // Propagate the new drag event. 2166 drag(time, x, y, dx, dy); 2167 2168 // Figure out if the user has scrolled down enough that they can scroll back up and 2169 // exit. 2170 if (mCurrentMode == Orientation.PORTRAIT) { 2171 // The cancelable threshold is determined by the top position of the tab in the 2172 // stack. 2173 final float discardOffset = mStackTabs[index].getScrollOffset(); 2174 final boolean beyondThreshold = -mScrollOffset < discardOffset; 2175 2176 // Allow the user to cancel in the future if they're beyond the threshold. 2177 mSwipeIsCancelable |= beyondThreshold; 2178 2179 // If the user can cancel the swipe and they're back behind the threshold, cancel. 2180 if (mSwipeIsCancelable && !beyondThreshold) swipeCancelled(time); 2181 } else { 2182 // The cancelable threshold is determined by the top position of the tab. 2183 final float discardOffset = mStackTabs[index].getLayoutTab().getY(); 2184 2185 boolean aboveThreshold = discardOffset < getRange(SWIPE_LANDSCAPE_THRESHOLD); 2186 2187 mSwipeIsCancelable |= !aboveThreshold; 2188 2189 if (mSwipeIsCancelable && aboveThreshold) swipeCancelled(time); 2190 } 2191 } 2192 2193 /** 2194 * Called when the swipe ends; most likely on finger up event. It gives a chance to start 2195 * an ending animation to exit the mode gracefully. 2196 * @param time The current time of the app in ms. 2197 */ 2198 public void swipeFinished(long time) { 2199 if (!mInSwipe) return; 2200 2201 mInSwipe = false; 2202 2203 onUpOrCancel(time); 2204 } 2205 2206 /** 2207 * Called when the user has cancelled a swipe; most likely if they have dragged their finger 2208 * back to the starting position. Some handlers will throw swipeFinished() instead. 2209 * @param time The current time of the app in ms. 2210 */ 2211 public void swipeCancelled(long time) { 2212 if (!mInSwipe) return; 2213 2214 mDiscardingTab = null; 2215 2216 mInSwipe = false; 2217 2218 // Select the current tab so we exit the switcher. 2219 Tab tab = TabModelUtils.getCurrentTab(mTabList); 2220 mLayout.uiSelectingTab(time, tab != null ? tab.getId() : Tab.INVALID_TAB_ID); 2221 } 2222 2223 /** 2224 * Fling from a swipe gesture. 2225 * @param time The current time of the app in ms. 2226 * @param x The horizontal coordinate the swipe is currently at in dp. 2227 * @param y The vertical coordinate the swipe is currently at in dp. 2228 * @param tx The horizontal difference between the start and the current position in dp. 2229 * @param ty The vertical difference between the start and the current position in dp. 2230 * @param vx The horizontal velocity of the fling. 2231 * @param vy The vertical velocity of the fling. 2232 */ 2233 public void swipeFlingOccurred( 2234 long time, float x, float y, float tx, float ty, float vx, float vy) { 2235 if (!mInSwipe) return; 2236 2237 // Propagate the fling data. 2238 fling(time, x, y, vx, vy); 2239 2240 onUpOrCancel(time); 2241 } 2242 2243 public static final FloatProperty<Stack> SCROLL_OFFSET = 2244 new FloatProperty<Stack>("SCROLL_OFFSET") { 2245 @Override 2246 public void setValue(Stack stack, float v) { 2247 stack.setScrollTarget(v, true); 2248 } 2249 2250 @Override 2251 public Float get(Stack stack) { 2252 return stack.getScrollOffset(); 2253 } 2254 }; 2255 } 2256