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.AnimatorSet;
9 import android.animation.TimeInterpolator;
10 
11 import androidx.annotation.IntDef;
12 
13 import org.chromium.base.MathUtils;
14 import org.chromium.chrome.browser.compositor.layouts.Layout.Orientation;
15 import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
16 import org.chromium.chrome.browser.flags.ChromeFeatureList;
17 import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
18 import org.chromium.chrome.browser.layouts.animation.CompositorAnimator;
19 import org.chromium.chrome.browser.layouts.animation.FloatProperty;
20 import org.chromium.ui.base.LocalizationUtils;
21 import org.chromium.ui.interpolators.BakedBezierInterpolator;
22 import org.chromium.ui.modelutil.PropertyModel;
23 
24 import java.lang.annotation.Retention;
25 import java.lang.annotation.RetentionPolicy;
26 import java.util.ArrayList;
27 
28 /**
29  * A factory that builds animations for the tab stack.
30  */
31 public class StackAnimation {
32     @IntDef({OverviewAnimationType.ENTER_STACK, OverviewAnimationType.NEW_TAB_OPENED,
33             OverviewAnimationType.TAB_FOCUSED, OverviewAnimationType.VIEW_MORE,
34             OverviewAnimationType.REACH_TOP, OverviewAnimationType.DISCARD,
35             OverviewAnimationType.DISCARD_ALL, OverviewAnimationType.UNDISCARD,
36             OverviewAnimationType.START_PINCH, OverviewAnimationType.FULL_ROLL,
37             OverviewAnimationType.NONE})
38     @Retention(RetentionPolicy.SOURCE)
39     public @interface OverviewAnimationType {
40         int ENTER_STACK = 0;
41         int NEW_TAB_OPENED = 1;
42         int TAB_FOCUSED = 2;
43         int VIEW_MORE = 3;
44         int REACH_TOP = 4;
45         // Commit/uncommit tab discard animations
46         int DISCARD = 5;
47         int DISCARD_ALL = 6;
48         int UNDISCARD = 7;
49         // Start pinch animation un-tilt all the tabs.
50         int START_PINCH = 8;
51         // Special animation
52         int FULL_ROLL = 9;
53         // Used for when the current state of the system is not animating
54         int NONE = 10;
55     }
56 
57     private static final int ENTER_STACK_ANIMATION_DURATION_MS = 300;
58     private static final int ENTER_STACK_BORDER_ALPHA_DURATION_MS = 200;
59     private static final int ENTER_STACK_RESIZE_DELAY_MS = 10;
60     private static final int ENTER_STACK_TOOLBAR_ALPHA_DURATION_MS = 100;
61     private static final int ENTER_STACK_TOOLBAR_ALPHA_DELAY_MS = 100;
62     private static final float ENTER_STACK_SIZE_RATIO = 0.35f;
63 
64     private static final int TAB_FOCUSED_ANIMATION_DURATION_MS = 400;
65     private static final int TAB_FOCUSED_BORDER_ALPHA_DURATION_MS = 200;
66     private static final int TAB_FOCUSED_TOOLBAR_ALPHA_DURATION_MS = 250;
67     private static final int TAB_FOCUSED_Y_STACK_DURATION_MS = 200;
68     private static final int TAB_FOCUSED_MAX_DELAY_MS = 100;
69 
70     private static final int VIEW_MORE_ANIMATION_DURATION_MS = 400;
71     private static final int VIEW_MORE_MIN_SIZE = 200;
72     private static final float VIEW_MORE_SIZE_RATIO = 0.75f;
73 
74     private static final int REACH_TOP_ANIMATION_DURATION_MS = 400;
75 
76     private static final int UNDISCARD_ANIMATION_DURATION_MS = 150;
77 
78     private static final int TAB_OPENED_ANIMATION_DURATION_MS = 300;
79 
80     private static final int DISCARD_ANIMATION_DURATION_MS = 150;
81 
82     private static final int TAB_REORDER_DURATION_MS = 500;
83     private static final int TAB_REORDER_START_SPAN = 400;
84 
85     private static final int START_PINCH_ANIMATION_DURATION_MS = 75;
86 
87     private static final int FULL_ROLL_ANIMATION_DURATION_MS = 1000;
88 
89     private final float mWidth;
90     private final float mHeight;
91     private final float mTopBrowserControlsHeight;
92     private final float mBorderTopHeight;
93     private final float mBorderTopOpaqueHeight;
94     private final float mBorderLeftWidth;
95     private final Stack mStack;
96     private final @Orientation int mOrientation;
97 
98     /**
99      * Protected constructor.
100      *
101      * @param stack                       The stack using the animations provided by this class.
102      * @param width                       The width of the layout in dp.
103      * @param height                      The height of the layout in dp.
104      * @param heightMinusBrowserControls  The height of the layout minus the browser controls in dp.
105      * @param borderFramePaddingTop       The top padding of the border frame in dp.
106      * @param borderFramePaddingTopOpaque The opaque top padding of the border frame in dp.
107      * @param borderFramePaddingLeft      The left padding of the border frame in dp.
108      */
StackAnimation(Stack stack, float width, float height, float topBrowserControlsHeight, float borderFramePaddingTop, float borderFramePaddingTopOpaque, float borderFramePaddingLeft, @Orientation int orientation)109     protected StackAnimation(Stack stack, float width, float height, float topBrowserControlsHeight,
110             float borderFramePaddingTop, float borderFramePaddingTopOpaque,
111             float borderFramePaddingLeft, @Orientation int orientation) {
112         mStack = stack;
113         mWidth = width;
114         mHeight = height;
115         mTopBrowserControlsHeight = topBrowserControlsHeight;
116         mOrientation = orientation;
117 
118         mBorderTopHeight = borderFramePaddingTop;
119         mBorderTopOpaqueHeight = borderFramePaddingTopOpaque;
120         mBorderLeftWidth = borderFramePaddingLeft;
121     }
122 
123     /**
124      * This is a wrapper for a {@link AnimatorSet} that plays a set of {@link CompositorAnimator}s
125      * at the same time, and it has the ability to cancel some {@link CompositorAnimator} animations
126      * as if it is needed.
127      */
128     class StackAnimatorSet {
129         private final ArrayList<Animator> mAnimationList = new ArrayList<>();
130         private final AnimatorSet mAnimatorSet = new AnimatorSet();
131         private final ArrayList<Animator> mCancelableAnimators = new ArrayList<>();
132         private final CompositorAnimationHandler mHandler;
133 
StackAnimatorSet(CompositorAnimationHandler handler)134         StackAnimatorSet(CompositorAnimationHandler handler) {
135             mHandler = handler;
136         }
137 
isPropertyCancelable(FloatProperty<T> property)138         <T> boolean isPropertyCancelable(FloatProperty<T> property) {
139             return property == StackTab.SCROLL_OFFSET;
140         }
141 
142         /**
143          * Helper method to create and add new {@link CompositorAnimator} to the set.
144          * @param target        Target associated with animated property.
145          * @param property      The property being animated.
146          * @param startValue    The starting value of the animation.
147          * @param endValue      The ending value of the animation.
148          * @param durationMs    The duration of the animation.
149          * @param startTimeMs   The start time.
150          * @param interpolator  The time interpolator for the animation. If it is null, will use the
151          *                      Interpolators.DECELERATE_INTERPOLATOR.
152          */
addToAnimationWithDelay(final T target, final FloatProperty<T> property, float startValue, float endValue, long durationMs, long startTimeMs, TimeInterpolator interpolator)153         <T> void addToAnimationWithDelay(final T target, final FloatProperty<T> property,
154                 float startValue, float endValue, long durationMs, long startTimeMs,
155                 TimeInterpolator interpolator) {
156             CompositorAnimator compositorAnimator;
157 
158             if (interpolator == null) {
159                 compositorAnimator = CompositorAnimator.ofFloatProperty(
160                         mHandler, target, property, startValue, endValue, durationMs);
161             } else {
162                 compositorAnimator = CompositorAnimator.ofFloatProperty(
163                         mHandler, target, property, startValue, endValue, durationMs, interpolator);
164             }
165             compositorAnimator.setStartDelay(startTimeMs);
166 
167             mAnimationList.add(compositorAnimator);
168 
169             if (isPropertyCancelable(property)) mCancelableAnimators.add(compositorAnimator);
170         }
171 
addToAnimation(final T target, final FloatProperty<T> property, float startValue, float endValue, long durationMs, TimeInterpolator interpolator)172         <T> void addToAnimation(final T target, final FloatProperty<T> property, float startValue,
173                 float endValue, long durationMs, TimeInterpolator interpolator) {
174             addToAnimationWithDelay(
175                     target, property, startValue, endValue, durationMs, 0, interpolator);
176         }
177 
addToAnimationWithDelay(final PropertyModel model, PropertyModel.WritableFloatPropertyKey key, float startValue, float endValue, long durationMs, long startTimeMs)178         void addToAnimationWithDelay(final PropertyModel model,
179                 PropertyModel.WritableFloatPropertyKey key, float startValue, float endValue,
180                 long durationMs, long startTimeMs) {
181             CompositorAnimator compositorAnimator = CompositorAnimator.ofWritableFloatPropertyKey(
182                     mHandler, model, key, startValue, endValue, durationMs);
183             compositorAnimator.setStartDelay(startTimeMs);
184 
185             mAnimationList.add(compositorAnimator);
186         }
187 
addToAnimation(final PropertyModel model, PropertyModel.WritableFloatPropertyKey key, float startValue, float endValue, long durationMs)188         void addToAnimation(final PropertyModel model, PropertyModel.WritableFloatPropertyKey key,
189                 float startValue, float endValue, long durationMs) {
190             addToAnimationWithDelay(model, key, startValue, endValue, durationMs, 0);
191         }
192 
193         /**
194          * Starts the {@link AnimatorSet} animation.
195          */
start()196         void start() {
197             mAnimatorSet.playTogether(mAnimationList);
198             mAnimatorSet.start();
199         }
200 
201         /**
202          * Cancels the cancelable animations.
203          */
cancelCancelableAnimators()204         void cancelCancelableAnimators() {
205             for (int i = 0; i < mCancelableAnimators.size(); i++) {
206                 mCancelableAnimators.get(i).cancel();
207             }
208         }
209 
210         /**
211          * {@see AnimatorSet#isRunning}.
212          * @return Whether the {@link AnimatorSet} is running.
213          */
isRunning()214         boolean isRunning() {
215             return mAnimatorSet.isRunning();
216         }
217 
218         /**
219          * Ends the {@link AnimatorSet} animations.
220          * {@see AnimatorSet#end}.
221          */
end()222         void end() {
223             mAnimatorSet.end();
224         }
225     }
226 
227     /**
228      * The wrapper method responsible for delegating the animations request to the appropriate
229      * helper method.  Not all parameters are used for each request.
230      *
231      * @param type          The type of animation to be created.  This is what
232      *                      determines which helper method is called.
233      * @param stack         The current stack.
234      * @param tabs          The tabs that make up the current stack that will
235      *                      be animated.
236      * @param focusIndex    The index of the tab that is the focus of this animation.
237      * @param sourceIndex   The index of the tab that triggered this animation.
238      * @param spacing       The default spacing between the tabs.
239      * @param discardRange  The range of the discard amount value.
240      * @return              The resulting AnimatorSet that will animate the tabs.
241      */
createAnimatorSetForType(@verviewAnimationType int type, Stack stack, StackTab[] tabs, int focusIndex, int sourceIndex, int spacing, float discardRange)242     public StackAnimatorSet createAnimatorSetForType(@OverviewAnimationType int type, Stack stack,
243             StackTab[] tabs, int focusIndex, int sourceIndex, int spacing, float discardRange) {
244         if (tabs == null) return null;
245 
246         StackAnimatorSet stackAnimatorSet = new StackAnimatorSet(stack.getAnimationHandler());
247 
248         switch (type) {
249             case OverviewAnimationType.DISCARD: // Purposeful fall through
250             case OverviewAnimationType.DISCARD_ALL: // Purposeful fall through
251             case OverviewAnimationType.UNDISCARD:
252                 createLandscapePortraitUpdateDiscardAnimatorSet(
253                         stackAnimatorSet, stack, tabs, spacing, discardRange);
254                 break;
255             case OverviewAnimationType.ENTER_STACK:
256                 // Responsible for generating the animations that shows the stack being entered.
257                 if (mOrientation == Orientation.LANDSCAPE) {
258                     createLandscapeEnterStackAnimatorSet(
259                             stackAnimatorSet, tabs, focusIndex, spacing);
260                 } else {
261                     createPortraitEnterStackAnimatorSet(
262                             stackAnimatorSet, tabs, focusIndex, spacing);
263                 }
264                 break;
265             case OverviewAnimationType.FULL_ROLL:
266                 // Responsible for generating the animations that make all the tabs do a full roll.
267                 for (int i = 0; i < tabs.length; ++i) {
268                     LayoutTab layoutTab = tabs[i].getLayoutTab();
269                     // Set the pivot
270                     layoutTab.setTiltX(
271                             layoutTab.getTiltX(), layoutTab.getScaledContentHeight() / 2.0f);
272                     layoutTab.setTiltY(
273                             layoutTab.getTiltY(), layoutTab.getScaledContentWidth() / 2.0f);
274                     // Create the angle animation
275                     addLandscapePortraitTiltScrollAnimation(
276                             stackAnimatorSet, layoutTab, -360.0f, FULL_ROLL_ANIMATION_DURATION_MS);
277                 }
278                 break;
279             case OverviewAnimationType.NEW_TAB_OPENED:
280                 // Responsible for generating the animations that shows a new tab being opened.
281                 if (mOrientation == Orientation.LANDSCAPE) return null;
282 
283                 for (int i = 0; i < tabs.length; i++) {
284                     stackAnimatorSet.addToAnimation(tabs[i], StackTab.SCROLL_OFFSET,
285                             tabs[i].getScrollOffset(), 0.0f, TAB_OPENED_ANIMATION_DURATION_MS,
286                             null);
287                 }
288                 break;
289             case OverviewAnimationType.REACH_TOP:
290                 // Responsible for generating the TabSwitcherAnimation that moves the tabs up so
291                 // they reach the to top the screen.
292                 float screenTarget = 0.0f;
293                 for (int i = 0; i < tabs.length; ++i) {
294                     if (screenTarget
295                             >= getLandscapePortraitScreenPositionInScrollDirection(tabs[i])) {
296                         break;
297                     }
298                     stackAnimatorSet.addToAnimation(tabs[i], StackTab.SCROLL_OFFSET,
299                             tabs[i].getScrollOffset(), mStack.screenToScroll(screenTarget),
300                             REACH_TOP_ANIMATION_DURATION_MS, null);
301                     screenTarget += mOrientation == Orientation.LANDSCAPE
302                             ? tabs[i].getLayoutTab().getScaledContentWidth()
303                             : tabs[i].getLayoutTab().getScaledContentHeight();
304                 }
305                 break;
306             case OverviewAnimationType.START_PINCH:
307                 // Responsible for generating the animations that flattens tabs when a pinch begins.
308                 for (int i = 0; i < tabs.length; ++i) {
309                     addLandscapePortraitTiltScrollAnimation(stackAnimatorSet,
310                             tabs[i].getLayoutTab(), 0, START_PINCH_ANIMATION_DURATION_MS);
311                 }
312                 break;
313             case OverviewAnimationType.TAB_FOCUSED:
314                 createLandscapePortraitTabFocusedAnimatorSet(
315                         stackAnimatorSet, tabs, focusIndex, spacing);
316                 break;
317             case OverviewAnimationType.VIEW_MORE:
318                 // Responsible for generating the animations that Shows more of the selected tab.
319                 if (sourceIndex + 1 >= tabs.length) return null;
320 
321                 float offset = mOrientation == Orientation.LANDSCAPE
322                         ? tabs[sourceIndex].getLayoutTab().getScaledContentWidth()
323                         : tabs[sourceIndex].getLayoutTab().getScaledContentHeight();
324                 offset = offset * VIEW_MORE_SIZE_RATIO + tabs[sourceIndex].getScrollOffset()
325                         - tabs[sourceIndex + 1].getScrollOffset();
326                 offset = Math.max(VIEW_MORE_MIN_SIZE, offset);
327 
328                 for (int i = sourceIndex + 1; i < tabs.length; ++i) {
329                     stackAnimatorSet.addToAnimation(tabs[i], StackTab.SCROLL_OFFSET,
330                             tabs[i].getScrollOffset(), tabs[i].getScrollOffset() + offset,
331                             VIEW_MORE_ANIMATION_DURATION_MS, null);
332                 }
333                 break;
334             default:
335                 return null;
336         }
337 
338         return stackAnimatorSet;
339     }
340 
getLandscapePortraitScreenPositionInScrollDirection(StackTab tab)341     private float getLandscapePortraitScreenPositionInScrollDirection(StackTab tab) {
342         return mOrientation == Orientation.LANDSCAPE ? tab.getLayoutTab().getX()
343                                                      : tab.getLayoutTab().getY();
344     }
345 
addLandscapePortraitTiltScrollAnimation( StackAnimatorSet stackAnimatorSet, LayoutTab tab, float end, int durationMs)346     private void addLandscapePortraitTiltScrollAnimation(
347             StackAnimatorSet stackAnimatorSet, LayoutTab tab, float end, int durationMs) {
348         if (mOrientation == Orientation.LANDSCAPE) {
349             stackAnimatorSet.addToAnimation(
350                     tab, LayoutTab.TILT_Y_IN_DEGREES, tab.getTiltY(), end, durationMs);
351         } else {
352             stackAnimatorSet.addToAnimation(
353                     tab, LayoutTab.TILT_X_IN_DEGREES, tab.getTiltX(), end, durationMs);
354         }
355     }
356 
357     // If this flag is enabled, we're using the non-overlapping tab switcher.
isHorizontalTabSwitcherFlagEnabled()358     private boolean isHorizontalTabSwitcherFlagEnabled() {
359         return ChromeFeatureList.isEnabled(ChromeFeatureList.HORIZONTAL_TAB_SWITCHER_ANDROID);
360     }
361 
createPortraitEnterStackAnimatorSet( StackAnimatorSet stackAnimatorSet, StackTab[] tabs, int focusIndex, int spacing)362     private void createPortraitEnterStackAnimatorSet(
363             StackAnimatorSet stackAnimatorSet, StackTab[] tabs, int focusIndex, int spacing) {
364         final float initialScrollOffset = mStack.screenToScroll(0);
365 
366         float trailingScrollOffset = 0.f;
367         if (focusIndex >= 0 && focusIndex < tabs.length - 1) {
368             final float focusOffset = tabs[focusIndex].getScrollOffset();
369             final float nextOffset = tabs[focusIndex + 1].getScrollOffset();
370             final float topSpacing = focusIndex == 0 ? spacing : 0.f;
371             final float extraSpace = tabs[focusIndex].getLayoutTab().getScaledContentHeight()
372                     * ENTER_STACK_SIZE_RATIO;
373             trailingScrollOffset = Math.max(focusOffset - nextOffset + topSpacing + extraSpace, 0);
374         }
375 
376         for (int i = 0; i < tabs.length; ++i) {
377             StackTab tab = tabs[i];
378 
379             tab.resetOffset();
380             tab.setScale(mStack.getScaleAmount());
381             tab.setAlpha(1.f);
382             tab.getLayoutTab().setToolbarAlpha(i == focusIndex ? 1.f : 0.f);
383             tab.getLayoutTab().setBorderScale(1.f);
384 
385             float scrollOffset = mStack.screenToScroll(i * spacing);
386 
387             if (i < focusIndex) {
388                 tab.getLayoutTab().setMaxContentHeight(mStack.getMaxTabHeight());
389                 stackAnimatorSet.addToAnimation(tab, StackTab.SCROLL_OFFSET, initialScrollOffset,
390                         scrollOffset, ENTER_STACK_ANIMATION_DURATION_MS, null);
391             } else if (i > focusIndex) {
392                 tab.getLayoutTab().setMaxContentHeight(mStack.getMaxTabHeight());
393                 tab.setScrollOffset(scrollOffset + trailingScrollOffset);
394                 stackAnimatorSet.addToAnimation(tab, StackTab.Y_IN_STACK_OFFSET, mHeight, 0,
395                         ENTER_STACK_ANIMATION_DURATION_MS, null);
396             } else { // i == focusIndex
397                 tab.setScrollOffset(scrollOffset);
398 
399                 stackAnimatorSet.addToAnimationWithDelay(tab.getLayoutTab(),
400                         LayoutTab.MAX_CONTENT_HEIGHT,
401                         tab.getLayoutTab().getUnclampedOriginalContentHeight(),
402                         mStack.getMaxTabHeight(), ENTER_STACK_ANIMATION_DURATION_MS,
403                         ENTER_STACK_RESIZE_DELAY_MS);
404                 stackAnimatorSet.addToAnimation(tab, StackTab.Y_IN_STACK_INFLUENCE, 0.0f, 1.0f,
405                         ENTER_STACK_BORDER_ALPHA_DURATION_MS, null);
406                 stackAnimatorSet.addToAnimation(tab, StackTab.SCALE, 1.0f, mStack.getScaleAmount(),
407                         ENTER_STACK_BORDER_ALPHA_DURATION_MS, null);
408                 stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.TOOLBAR_Y_OFFSET, 0.f,
409                         getToolbarOffsetToLineUpWithBorder(), ENTER_STACK_BORDER_ALPHA_DURATION_MS);
410                 stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.SIDE_BORDER_SCALE,
411                         0.f, 1.f, ENTER_STACK_BORDER_ALPHA_DURATION_MS);
412 
413                 stackAnimatorSet.addToAnimationWithDelay(tab.getLayoutTab(),
414                         LayoutTab.TOOLBAR_ALPHA, 1.f, 0.f, ENTER_STACK_BORDER_ALPHA_DURATION_MS,
415                         ENTER_STACK_TOOLBAR_ALPHA_DELAY_MS);
416 
417                 tab.setYOutOfStack(getStaticTabPosition());
418             }
419         }
420     }
421 
createLandscapeEnterStackAnimatorSet( StackAnimatorSet stackAnimatorSet, StackTab[] tabs, int focusIndex, int spacing)422     private void createLandscapeEnterStackAnimatorSet(
423             StackAnimatorSet stackAnimatorSet, StackTab[] tabs, int focusIndex, int spacing) {
424         final float initialScrollOffset = mStack.screenToScroll(0);
425 
426         for (int i = 0; i < tabs.length; ++i) {
427             StackTab tab = tabs[i];
428 
429             tab.resetOffset();
430             tab.setScale(mStack.getScaleAmount());
431             tab.setAlpha(1.f);
432             tab.getLayoutTab().setToolbarAlpha(i == focusIndex ? 1.f : 0.f);
433             tab.getLayoutTab().setBorderScale(1.f);
434 
435             final float scrollOffset = mStack.screenToScroll(i * spacing);
436 
437             stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.MAX_CONTENT_HEIGHT,
438                     tab.getLayoutTab().getUnclampedOriginalContentHeight(),
439                     mStack.getMaxTabHeight(), ENTER_STACK_ANIMATION_DURATION_MS);
440             if (i < focusIndex) {
441                 stackAnimatorSet.addToAnimation(tab, StackTab.SCROLL_OFFSET, initialScrollOffset,
442                         scrollOffset, ENTER_STACK_ANIMATION_DURATION_MS, null);
443             } else if (i > focusIndex) {
444                 tab.setScrollOffset(scrollOffset);
445                 stackAnimatorSet.addToAnimation(tab, StackTab.X_IN_STACK_OFFSET,
446                         (mWidth > mHeight && LocalizationUtils.isLayoutRtl()) ? -mWidth : mWidth,
447                         0.0f, ENTER_STACK_ANIMATION_DURATION_MS, null);
448             } else { // i == focusIndex
449                 tab.setScrollOffset(scrollOffset);
450 
451                 stackAnimatorSet.addToAnimation(tab, StackTab.X_IN_STACK_INFLUENCE, 0.0f, 1.0f,
452                         ENTER_STACK_BORDER_ALPHA_DURATION_MS, null);
453                 stackAnimatorSet.addToAnimation(tab, StackTab.SCALE, 1.0f, mStack.getScaleAmount(),
454                         ENTER_STACK_BORDER_ALPHA_DURATION_MS, null);
455                 stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.TOOLBAR_Y_OFFSET, 0.f,
456                         getToolbarOffsetToLineUpWithBorder(), ENTER_STACK_BORDER_ALPHA_DURATION_MS);
457                 stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.SIDE_BORDER_SCALE,
458                         0.f, 1.f, ENTER_STACK_BORDER_ALPHA_DURATION_MS);
459 
460                 stackAnimatorSet.addToAnimationWithDelay(tab.getLayoutTab(),
461                         LayoutTab.TOOLBAR_ALPHA, 1.f, 0.f, ENTER_STACK_TOOLBAR_ALPHA_DURATION_MS,
462                         ENTER_STACK_TOOLBAR_ALPHA_DELAY_MS);
463             }
464         }
465     }
466 
467     /**
468      * Responsible for generating the animations that shows a tab being
469      * focused (the stack is being left).
470      * @param stackAnimatorSet {@link StackAnimatorSet} for created animations.
471      * @param tabs          The tabs that make up the stack.  These are the
472      *                      tabs that will be affected by the TabSwitcherAnimation.
473      * @param focusIndex    The focused index.  In this case, this is the index of
474      *                      the tab clicked and is being brought up to view.
475      * @param spacing       The default spacing between tabs.
476      */
createLandscapePortraitTabFocusedAnimatorSet( StackAnimatorSet stackAnimatorSet, StackTab[] tabs, int focusIndex, int spacing)477     private void createLandscapePortraitTabFocusedAnimatorSet(
478             StackAnimatorSet stackAnimatorSet, StackTab[] tabs, int focusIndex, int spacing) {
479         for (int i = 0; i < tabs.length; ++i) {
480             StackTab tab = tabs[i];
481             LayoutTab layoutTab = tab.getLayoutTab();
482 
483             addLandscapePortraitTiltScrollAnimation(
484                     stackAnimatorSet, layoutTab, 0.0f, TAB_FOCUSED_ANIMATION_DURATION_MS);
485             stackAnimatorSet.addToAnimation(tab, StackTab.DISCARD_AMOUNT, tab.getDiscardAmount(),
486                     0.0f, TAB_FOCUSED_ANIMATION_DURATION_MS, null);
487 
488             if (i < focusIndex) {
489                 // Landscape: for tabs left of the focused tab move them left to 0.
490                 // Portrait: for tabs above the focused tab move them up to 0.
491                 stackAnimatorSet.addToAnimation(tab, StackTab.SCROLL_OFFSET, tab.getScrollOffset(),
492                         mOrientation == Orientation.LANDSCAPE
493                                 ? Math.max(0.0f, tab.getScrollOffset() - mWidth - spacing)
494                                 : tab.getScrollOffset() - mHeight - spacing,
495                         TAB_FOCUSED_ANIMATION_DURATION_MS, null);
496                 continue;
497             } else if (i > focusIndex) {
498                 if (mOrientation == Orientation.LANDSCAPE) {
499                     // We also need to animate the X Translation to move them right
500                     // off the screen.
501                     float coveringTabPosition = layoutTab.getX();
502                     float distanceToBorder = LocalizationUtils.isLayoutRtl()
503                             ? coveringTabPosition + layoutTab.getScaledContentWidth()
504                             : mWidth - coveringTabPosition;
505                     float clampedDistanceToBorder = MathUtils.clamp(distanceToBorder, 0, mWidth);
506                     float delay = TAB_FOCUSED_MAX_DELAY_MS * clampedDistanceToBorder / mWidth;
507                     stackAnimatorSet.addToAnimationWithDelay(tab, StackTab.X_IN_STACK_OFFSET,
508                             tab.getXInStackOffset(),
509                             tab.getXInStackOffset()
510                                     + (LocalizationUtils.isLayoutRtl() ? -mWidth : mWidth),
511                             (TAB_FOCUSED_ANIMATION_DURATION_MS - (long) delay), (long) delay, null);
512                 } else { // mOrientation == Orientation.PORTRAIT
513                     // We also need to animate the Y Translation to move them down
514                     // off the screen.
515                     float coveringTabPosition = layoutTab.getY();
516                     float distanceToBorder =
517                             MathUtils.clamp(mHeight - coveringTabPosition, 0, mHeight);
518                     float delay = TAB_FOCUSED_MAX_DELAY_MS * distanceToBorder / mHeight;
519                     stackAnimatorSet.addToAnimationWithDelay(tab, StackTab.Y_IN_STACK_OFFSET,
520                             tab.getYInStackOffset(), tab.getYInStackOffset() + mHeight,
521                             (TAB_FOCUSED_ANIMATION_DURATION_MS - (long) delay), (long) delay, null);
522                 }
523                 continue;
524             }
525 
526             // This is the focused tab.  We need to scale it back to
527             // 1.0f, move it to the top of the screen, and animate the
528             // X Translation (for Landscape) / Y Translation (for Portrait) so that it looks like it
529             // is zooming into the full screen view.
530             //
531             // In Landscape we additionally move the card to the top left and extend it out so it
532             // becomes a full card.
533             tab.setXOutOfStack(0);
534             tab.setYOutOfStack(0.0f);
535             layoutTab.setBorderScale(1.f);
536 
537             if (mOrientation == Orientation.LANDSCAPE) {
538                 stackAnimatorSet.addToAnimation(tab, StackTab.X_IN_STACK_INFLUENCE,
539                         tab.getXInStackInfluence(), 0.0f, TAB_FOCUSED_ANIMATION_DURATION_MS, null);
540                 if (!isHorizontalTabSwitcherFlagEnabled()) {
541                     stackAnimatorSet.addToAnimation(tab, StackTab.SCROLL_OFFSET,
542                             tab.getScrollOffset(), mStack.screenToScroll(0),
543                             TAB_FOCUSED_ANIMATION_DURATION_MS, null);
544                 }
545             } else { // mOrientation == Orientation.PORTRAIT
546                 stackAnimatorSet.addToAnimation(tab, StackTab.SCROLL_OFFSET, tab.getScrollOffset(),
547                         Math.max(0.0f, tab.getScrollOffset() - mWidth - spacing),
548                         TAB_FOCUSED_ANIMATION_DURATION_MS, null);
549             }
550 
551             stackAnimatorSet.addToAnimation(tab, StackTab.SCALE, tab.getScale(), 1.0f,
552                     TAB_FOCUSED_ANIMATION_DURATION_MS, null);
553             stackAnimatorSet.addToAnimation(tab, StackTab.Y_IN_STACK_INFLUENCE,
554                     tab.getYInStackInfluence(), 0.0f, TAB_FOCUSED_Y_STACK_DURATION_MS, null);
555             stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.MAX_CONTENT_HEIGHT,
556                     tab.getLayoutTab().getMaxContentHeight(),
557                     tab.getLayoutTab().getUnclampedOriginalContentHeight(),
558                     TAB_FOCUSED_ANIMATION_DURATION_MS);
559 
560             tab.setYOutOfStack(getStaticTabPosition());
561 
562             if (layoutTab.shouldStall()) {
563                 stackAnimatorSet.addToAnimation(layoutTab, LayoutTab.SATURATION, 1.0f, 0.0f,
564                         TAB_FOCUSED_BORDER_ALPHA_DURATION_MS);
565             }
566             stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.TOOLBAR_ALPHA,
567                     layoutTab.getToolbarAlpha(), 1.f, TAB_FOCUSED_TOOLBAR_ALPHA_DURATION_MS);
568             stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.TOOLBAR_Y_OFFSET,
569                     getToolbarOffsetToLineUpWithBorder(), 0.f,
570                     TAB_FOCUSED_TOOLBAR_ALPHA_DURATION_MS);
571             stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.SIDE_BORDER_SCALE, 1.f,
572                     0.f, TAB_FOCUSED_TOOLBAR_ALPHA_DURATION_MS);
573         }
574     }
575 
576     /**
577      * Responsible for generating the animations that moves the tabs back in from
578      * discard attempt or commit the current discard (if any). It also re-even the tabs
579      * if one of then is removed.
580      * @param stackAnimatorSet {@link StackAnimatorSet} for created animations.
581      * @param stack         Stack.
582      * @param tabs          The tabs that make up the stack. These are the
583      *                      tabs that will be affected by the TabSwitcherAnimation.
584      * @param spacing       The default spacing between tabs.
585      * @param discardRange  The maximum value the discard amount.
586      */
createLandscapePortraitUpdateDiscardAnimatorSet(StackAnimatorSet stackAnimatorSet, Stack stack, StackTab[] tabs, int spacing, float discardRange)587     private void createLandscapePortraitUpdateDiscardAnimatorSet(StackAnimatorSet stackAnimatorSet,
588             Stack stack, StackTab[] tabs, int spacing, float discardRange) {
589         int dyingTabsCount = 0;
590         int firstDyingTabIndex = -1;
591         float firstDyingTabOffset = 0;
592         for (int i = 0; i < tabs.length; ++i) {
593             addLandscapePortraitTiltScrollAnimation(stackAnimatorSet, tabs[i].getLayoutTab(), 0.0f,
594                     UNDISCARD_ANIMATION_DURATION_MS);
595 
596             if (tabs[i].isDying()) {
597                 dyingTabsCount++;
598                 if (dyingTabsCount == 1) {
599                     firstDyingTabIndex = i;
600                     firstDyingTabOffset =
601                             getLandscapePortraitScreenPositionInScrollDirection(tabs[i]);
602                 }
603             }
604         }
605 
606         float screenSizeInScrollDirection =
607                 mOrientation == Orientation.LANDSCAPE ? mWidth : mHeight;
608 
609         // This is used to determine the discard direction when user just clicks X to close a
610         // tab. On portrait, positive direction (x) is right hand side (on clicking the close
611         // button, discard the tab to the right on LTR, to the left on RTL). On landscape,
612         // positive direction (y) is towards bottom.
613         boolean defaultDiscardDirectionPositive =
614                 mOrientation == Orientation.LANDSCAPE ? true : !LocalizationUtils.isLayoutRtl();
615 
616         int newIndex = 0;
617         for (int i = 0; i < tabs.length; ++i) {
618             StackTab tab = tabs[i];
619             // If the non-overlapping horizontal tab switcher is enabled, we shift all the
620             // tabs over simultaneously. Otherwise we stagger the animation start times to
621             // create a ripple effect.
622             long startTime = isHorizontalTabSwitcherFlagEnabled()
623                     ? 0
624                     : (long) Math.max(0,
625                             TAB_REORDER_START_SPAN / screenSizeInScrollDirection
626                                     * (getLandscapePortraitScreenPositionInScrollDirection(tab)
627                                             - firstDyingTabOffset));
628             if (tab.isDying()) {
629                 float discard = tab.getDiscardAmount();
630                 if (discard == 0.0f) discard = defaultDiscardDirectionPositive ? 0.0f : -0.0f;
631                 float s = Math.copySign(1.0f, discard);
632                 long duration = (long) (DISCARD_ANIMATION_DURATION_MS
633                         * (1.0f - Math.abs(discard / discardRange)));
634 
635                 stackAnimatorSet.addToAnimation(tab, StackTab.DISCARD_AMOUNT, discard,
636                         discardRange * s, duration, BakedBezierInterpolator.FADE_OUT_CURVE);
637             } else {
638                 if (tab.getDiscardAmount() != 0.f) {
639                     stackAnimatorSet.addToAnimation(tab, StackTab.DISCARD_AMOUNT,
640                             tab.getDiscardAmount(), 0.0f, UNDISCARD_ANIMATION_DURATION_MS, null);
641                 }
642                 stackAnimatorSet.addToAnimation(tab, StackTab.SCALE, tab.getScale(),
643                         mStack.getScaleAmount(), DISCARD_ANIMATION_DURATION_MS, null);
644 
645                 stackAnimatorSet.addToAnimation(tab.getLayoutTab(), LayoutTab.MAX_CONTENT_HEIGHT,
646                         tab.getLayoutTab().getMaxContentHeight(), mStack.getMaxTabHeight(),
647                         DISCARD_ANIMATION_DURATION_MS);
648 
649                 float newScrollOffset = mStack.screenToScroll(spacing * newIndex);
650 
651                 // If the tab is not dying we want to readjust it's position
652                 // based on the new spacing requirements.  For a fully discarded tab, just
653                 // put it in the right place.
654                 if (tab.getDiscardAmount() >= discardRange) {
655                     tab.setScrollOffset(newScrollOffset);
656                     tab.setScale(mStack.getScaleAmount());
657                 } else {
658                     float start = tab.getScrollOffset();
659                     if (start != newScrollOffset) {
660                         stackAnimatorSet.addToAnimation(tab, StackTab.SCROLL_OFFSET, start,
661                                 newScrollOffset, TAB_REORDER_DURATION_MS, null);
662                     }
663                 }
664                 newIndex++;
665             }
666         }
667 
668         // Scroll offset animation for non-overlapping horizontal tab switcher (if enabled)
669         if (isHorizontalTabSwitcherFlagEnabled()) {
670             NonOverlappingStack nonOverlappingStack = (NonOverlappingStack) stack;
671             int centeredTabIndex = nonOverlappingStack.getCenteredTabIndex();
672 
673             // For all tab closures (except for the last one), we slide the remaining tabs
674             // in to fill the gap.
675             //
676             // There are two cases where we also need to animate the NonOverlappingStack's
677             // overall scroll position over by one tab:
678             //
679             // - Closing the last tab while centered on it (since we don't have a tab we can
680             //   slide over to replace it)
681             //
682             // - Closing any tab prior to the currently centered one (so we can keep the
683             //   same tab centered). Together with animating the individual scroll offsets for
684             //   each tab, this has the visual appearance of sliding in the prior tabs from the
685             //   left (in LTR mode) to fill the gap.
686             boolean closingLastTabWhileCentered =
687                     firstDyingTabIndex == tabs.length - 1 && firstDyingTabIndex == centeredTabIndex;
688             boolean closingPriorTab =
689                     firstDyingTabIndex != -1 && firstDyingTabIndex < centeredTabIndex;
690 
691             boolean shouldAnimateStackScrollOffset = closingLastTabWhileCentered || closingPriorTab;
692 
693             if (shouldAnimateStackScrollOffset) {
694                 nonOverlappingStack.suppressScrollClampingForAnimation();
695                 stackAnimatorSet.addToAnimation(nonOverlappingStack, Stack.SCROLL_OFFSET,
696                         stack.getScrollOffset(), -(centeredTabIndex - 1) * stack.getSpacing(),
697                         TAB_REORDER_DURATION_MS, null);
698             }
699         }
700     }
701 
702     /**
703      * @return The offset for the toolbar to line the top up with the opaque component of
704      *         the border.
705      */
706     private float getToolbarOffsetToLineUpWithBorder() {
707         return mTopBrowserControlsHeight - mBorderTopOpaqueHeight;
708     }
709 
710     /**
711      * @return The position of the static tab when entering or exiting the tab switcher.
712      */
713     private float getStaticTabPosition() {
714         return mTopBrowserControlsHeight - mBorderTopHeight;
715     }
716 }
717