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