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