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;
6 
7 import android.animation.Animator;
8 import android.animation.AnimatorListenerAdapter;
9 import android.animation.AnimatorSet;
10 import android.content.Context;
11 import android.graphics.RectF;
12 
13 import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
14 import org.chromium.chrome.browser.compositor.LayerTitleCache;
15 import org.chromium.chrome.browser.compositor.layouts.Layout;
16 import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost;
17 import org.chromium.chrome.browser.compositor.layouts.LayoutUpdateHost;
18 import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
19 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
20 import org.chromium.chrome.browser.compositor.layouts.eventfilter.BlackHoleEventFilter;
21 import org.chromium.chrome.browser.compositor.layouts.phone.stack.Stack;
22 import org.chromium.chrome.browser.compositor.scene_layer.TabListSceneLayer;
23 import org.chromium.chrome.browser.layouts.EventFilter;
24 import org.chromium.chrome.browser.layouts.LayoutType;
25 import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
26 import org.chromium.chrome.browser.layouts.animation.CompositorAnimator;
27 import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
28 import org.chromium.chrome.browser.tab.Tab;
29 import org.chromium.chrome.browser.tabmodel.TabModel;
30 import org.chromium.ui.interpolators.BakedBezierInterpolator;
31 import org.chromium.ui.resources.ResourceManager;
32 
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.LinkedList;
37 
38 /**
39  * This class handles animating the opening of new tabs.
40  */
41 public class SimpleAnimationLayout extends Layout {
42     /** Animation for discarding a tab. */
43     private CompositorAnimator mDiscardAnimator;
44 
45     /** The animation for a tab being created in the foreground. */
46     private AnimatorSet mTabCreatedForegroundAnimation;
47 
48     /** The animation for a tab being created in the background. */
49     private AnimatorSet mTabCreatedBackgroundAnimation;
50 
51     /** Fraction to scale tabs by during animation. */
52     public static final float SCALE_FRACTION = 0.90f;
53 
54     /** Duration of the first step of the background animation: zooming out, rotating in */
55     private static final long BACKGROUND_STEP1_DURATION = 300;
56     /** Duration of the second step of the background animation: pause */
57     private static final long BACKGROUND_STEP2_DURATION = 150;
58     /** Duration of the third step of the background animation: zooming in, sliding out */
59     private static final long BACKGROUND_STEP3_DURATION = 300;
60     /** Percentage of the screen covered by the new tab */
61     private static final float BACKGROUND_COVER_PCTG = 0.5f;
62 
63     /** The time duration of the animation */
64     protected static final int FOREGROUND_ANIMATION_DURATION = 300;
65 
66     /** The time duration of the animation */
67     protected static final int TAB_CLOSED_ANIMATION_DURATION = 250;
68 
69     /**
70      * A cached {@link LayoutTab} representation of the currently closing tab. If it's not
71      * null, it means tabClosing() has been called to start animation setup but
72      * tabClosed() has not yet been called to finish animation startup
73      */
74     private LayoutTab mClosedTab;
75 
76     private LayoutTab mAnimatedTab;
77     private final TabListSceneLayer mSceneLayer;
78     private final BlackHoleEventFilter mBlackHoleEventFilter;
79 
80     /**
81      * Creates an instance of the {@link SimpleAnimationLayout}.
82      * @param context     The current Android's context.
83      * @param updateHost  The {@link LayoutUpdateHost} view for this layout.
84      * @param renderHost  The {@link LayoutRenderHost} view for this layout.
85      */
SimpleAnimationLayout( Context context, LayoutUpdateHost updateHost, LayoutRenderHost renderHost)86     public SimpleAnimationLayout(
87             Context context, LayoutUpdateHost updateHost, LayoutRenderHost renderHost) {
88         super(context, updateHost, renderHost);
89         mBlackHoleEventFilter = new BlackHoleEventFilter(context);
90         mSceneLayer = new TabListSceneLayer();
91     }
92 
93     @Override
getViewportMode()94     public @ViewportMode int getViewportMode() {
95         return ViewportMode.USE_PREVIOUS_BROWSER_CONTROLS_STATE;
96     }
97 
98     @Override
show(long time, boolean animate)99     public void show(long time, boolean animate) {
100         super.show(time, animate);
101 
102         if (mTabModelSelector != null && mTabContentManager != null) {
103             Tab tab = mTabModelSelector.getCurrentTab();
104             if (tab != null && tab.isNativePage()) mTabContentManager.cacheTabThumbnail(tab);
105         }
106 
107         reset();
108     }
109 
110     @Override
handlesTabCreating()111     public boolean handlesTabCreating() {
112         return true;
113     }
114 
115     @Override
handlesTabClosing()116     public boolean handlesTabClosing() {
117         return true;
118     }
119 
120     @Override
updateLayout(long time, long dt)121     protected void updateLayout(long time, long dt) {
122         super.updateLayout(time, dt);
123         if (mLayoutTabs == null) return;
124         boolean needUpdate = false;
125         for (int i = mLayoutTabs.length - 1; i >= 0; i--) {
126             needUpdate = updateSnap(dt, mLayoutTabs[i]) || needUpdate;
127         }
128         if (needUpdate) requestUpdate();
129     }
130 
131     @Override
onTabCreating(int sourceTabId)132     public void onTabCreating(int sourceTabId) {
133         super.onTabCreating(sourceTabId);
134         reset();
135 
136         // Make sure any currently running animations can't influence tab if we are reusing it.
137         forceAnimationToFinish();
138 
139         ensureSourceTabCreated(sourceTabId);
140     }
141 
ensureSourceTabCreated(int sourceTabId)142     private void ensureSourceTabCreated(int sourceTabId) {
143         if (mLayoutTabs != null && mLayoutTabs.length == 1
144                 && mLayoutTabs[0].getId() == sourceTabId) {
145             return;
146         }
147         // Just draw the source tab on the screen.
148         TabModel sourceModel = mTabModelSelector.getModelForTabId(sourceTabId);
149         if (sourceModel == null) return;
150         LayoutTab sourceLayoutTab =
151                 createLayoutTab(sourceTabId, sourceModel.isIncognito(), NO_CLOSE_BUTTON, NO_TITLE);
152         sourceLayoutTab.setBorderAlpha(0.0f);
153 
154         mLayoutTabs = new LayoutTab[] {sourceLayoutTab};
155         updateCacheVisibleIds(new LinkedList<Integer>(Arrays.asList(sourceTabId)));
156     }
157 
158     @Override
onTabCreated(long time, int id, int index, int sourceId, boolean newIsIncognito, boolean background, float originX, float originY)159     public void onTabCreated(long time, int id, int index, int sourceId, boolean newIsIncognito,
160             boolean background, float originX, float originY) {
161         super.onTabCreated(time, id, index, sourceId, newIsIncognito, background, originX, originY);
162         ensureSourceTabCreated(sourceId);
163         if (background && mLayoutTabs != null && mLayoutTabs.length > 0) {
164             tabCreatedInBackground(id, sourceId, newIsIncognito, originX, originY);
165         } else {
166             tabCreatedInForeground(id, sourceId, newIsIncognito, originX, originY);
167         }
168     }
169 
170     /**
171      * Animate opening a tab in the foreground.
172      *
173      * @param id             The id of the new tab to animate.
174      * @param sourceId       The id of the tab that spawned this new tab.
175      * @param newIsIncognito true if the new tab is an incognito tab.
176      * @param originX        The X coordinate of the last touch down event that spawned this tab.
177      * @param originY        The Y coordinate of the last touch down event that spawned this tab.
178      */
tabCreatedInForeground( int id, int sourceId, boolean newIsIncognito, float originX, float originY)179     private void tabCreatedInForeground(
180             int id, int sourceId, boolean newIsIncognito, float originX, float originY) {
181         LayoutTab newLayoutTab = createLayoutTab(id, newIsIncognito, NO_CLOSE_BUTTON, NO_TITLE);
182         if (mLayoutTabs == null || mLayoutTabs.length == 0) {
183             mLayoutTabs = new LayoutTab[] {newLayoutTab};
184         } else {
185             mLayoutTabs = new LayoutTab[] {mLayoutTabs[0], newLayoutTab};
186         }
187         updateCacheVisibleIds(new LinkedList<Integer>(Arrays.asList(id, sourceId)));
188 
189         newLayoutTab.setBorderAlpha(0.0f);
190         newLayoutTab.setStaticToViewBlend(1.f);
191 
192         forceAnimationToFinish();
193 
194         CompositorAnimationHandler handler = getAnimationHandler();
195         CompositorAnimator scaleAnimation = CompositorAnimator.ofWritableFloatPropertyKey(
196                 handler, newLayoutTab, LayoutTab.SCALE, 0f, 1f, FOREGROUND_ANIMATION_DURATION);
197 
198         CompositorAnimator alphaAnimation = CompositorAnimator.ofWritableFloatPropertyKey(
199                 handler, newLayoutTab, LayoutTab.ALPHA, 0f, 1f, FOREGROUND_ANIMATION_DURATION);
200 
201         CompositorAnimator xAnimation = CompositorAnimator.ofWritableFloatPropertyKey(
202                 handler, newLayoutTab, LayoutTab.X, originX, 0f, FOREGROUND_ANIMATION_DURATION);
203         CompositorAnimator yAnimation = CompositorAnimator.ofWritableFloatPropertyKey(
204                 handler, newLayoutTab, LayoutTab.Y, originY, 0f, FOREGROUND_ANIMATION_DURATION);
205 
206         mTabCreatedForegroundAnimation = new AnimatorSet();
207         mTabCreatedForegroundAnimation.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE);
208         mTabCreatedForegroundAnimation.playTogether(
209                 scaleAnimation, alphaAnimation, xAnimation, yAnimation);
210         mTabCreatedForegroundAnimation.start();
211 
212         mTabModelSelector.selectModel(newIsIncognito);
213         startHiding(id, false);
214     }
215 
216     /**
217      * Animate opening a tab in the background.
218      *
219      * @param id             The id of the new tab to animate.
220      * @param sourceId       The id of the tab that spawned this new tab.
221      * @param newIsIncognito true if the new tab is an incognito tab.
222      * @param originX        The X screen coordinate in dp of the last touch down event that spawned
223      *                       this tab.
224      * @param originY        The Y screen coordinate in dp of the last touch down event that spawned
225      *                       this tab.
226      */
tabCreatedInBackground( int id, int sourceId, boolean newIsIncognito, float originX, float originY)227     private void tabCreatedInBackground(
228             int id, int sourceId, boolean newIsIncognito, float originX, float originY) {
229         LayoutTab newLayoutTab = createLayoutTab(id, newIsIncognito, NO_CLOSE_BUTTON, NEED_TITLE);
230         // mLayoutTabs should already have the source tab from tabCreating().
231         assert mLayoutTabs.length == 1;
232         LayoutTab sourceLayoutTab = mLayoutTabs[0];
233         mLayoutTabs = new LayoutTab[] {sourceLayoutTab, newLayoutTab};
234         updateCacheVisibleIds(new LinkedList<Integer>(Arrays.asList(id, sourceId)));
235 
236         forceAnimationToFinish();
237 
238         newLayoutTab.setBorderAlpha(0.0f);
239         final float scale = SCALE_FRACTION;
240         final float margin = Math.min(getWidth(), getHeight()) * (1.0f - scale) / 2.0f;
241 
242         CompositorAnimationHandler handler = getAnimationHandler();
243         Collection<Animator> animationList = new ArrayList<>(5);
244 
245         // Step 1: zoom out the source tab and bring in the new tab
246         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(
247                 handler, sourceLayoutTab, LayoutTab.SCALE, 1f, scale, BACKGROUND_STEP1_DURATION));
248         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(
249                 handler, sourceLayoutTab, LayoutTab.X, 0f, margin, BACKGROUND_STEP1_DURATION));
250         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(
251                 handler, sourceLayoutTab, LayoutTab.Y, 0f, margin, BACKGROUND_STEP1_DURATION));
252         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, sourceLayoutTab,
253                 LayoutTab.BORDER_SCALE, 1f / scale, 1f, BACKGROUND_STEP1_DURATION));
254         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, sourceLayoutTab,
255                 LayoutTab.BORDER_ALPHA, 0f, 1f, BACKGROUND_STEP1_DURATION));
256 
257         AnimatorSet step1Source = new AnimatorSet();
258         step1Source.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE);
259         step1Source.playTogether(animationList);
260 
261         float pauseX = margin;
262         float pauseY = margin;
263         if (getOrientation() == Orientation.PORTRAIT) {
264             pauseY = BACKGROUND_COVER_PCTG * getHeight();
265         } else {
266             pauseX = BACKGROUND_COVER_PCTG * getWidth();
267         }
268 
269         animationList = new ArrayList<>(4);
270 
271         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(
272                 handler, newLayoutTab, LayoutTab.ALPHA, 0f, 1f, BACKGROUND_STEP1_DURATION / 2));
273 
274         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(
275                 handler, newLayoutTab, LayoutTab.SCALE, 0f, scale, BACKGROUND_STEP1_DURATION));
276         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(
277                 handler, newLayoutTab, LayoutTab.X, originX, pauseX, BACKGROUND_STEP1_DURATION));
278         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(
279                 handler, newLayoutTab, LayoutTab.Y, originY, pauseY, BACKGROUND_STEP1_DURATION));
280 
281         AnimatorSet step1New = new AnimatorSet();
282         step1New.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
283         step1New.playTogether(animationList);
284 
285         AnimatorSet step1 = new AnimatorSet();
286         step1.playTogether(step1New, step1Source);
287 
288         // step 2: pause and admire the nice tabs
289 
290         // step 3: zoom in the source tab and slide down the new tab
291         animationList = new ArrayList<>(7);
292         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, sourceLayoutTab,
293                 LayoutTab.SCALE, scale, 1f, BACKGROUND_STEP3_DURATION,
294                 BakedBezierInterpolator.TRANSFORM_CURVE));
295         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, sourceLayoutTab,
296                 LayoutTab.X, margin, 0f, BACKGROUND_STEP3_DURATION,
297                 BakedBezierInterpolator.TRANSFORM_CURVE));
298         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, sourceLayoutTab,
299                 LayoutTab.Y, margin, 0f, BACKGROUND_STEP3_DURATION,
300                 BakedBezierInterpolator.TRANSFORM_CURVE));
301         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, sourceLayoutTab,
302                 LayoutTab.BORDER_SCALE, 1f, 1f / scale, BACKGROUND_STEP3_DURATION,
303                 BakedBezierInterpolator.TRANSFORM_CURVE));
304         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, sourceLayoutTab,
305                 LayoutTab.BORDER_ALPHA, 1f, 0f, BACKGROUND_STEP3_DURATION,
306                 BakedBezierInterpolator.TRANSFORM_CURVE));
307 
308         animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(
309                 handler, newLayoutTab, LayoutTab.ALPHA, 1f, 0f, BACKGROUND_STEP3_DURATION));
310 
311         if (getOrientation() == Orientation.PORTRAIT) {
312             animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, newLayoutTab,
313                     LayoutTab.Y, pauseY, getHeight(), BACKGROUND_STEP3_DURATION,
314                     BakedBezierInterpolator.FADE_OUT_CURVE));
315         } else {
316             animationList.add(CompositorAnimator.ofWritableFloatPropertyKey(handler, newLayoutTab,
317                     LayoutTab.X, pauseX, getWidth(), BACKGROUND_STEP3_DURATION,
318                     BakedBezierInterpolator.FADE_OUT_CURVE));
319         }
320 
321         AnimatorSet step3 = new AnimatorSet();
322         step3.setStartDelay(BACKGROUND_STEP2_DURATION);
323         step3.addListener(new AnimatorListenerAdapter() {
324             @Override
325             public void onAnimationEnd(Animator animation) {
326                 // Once the animation has finished, we can switch layouts.
327                 startHiding(sourceId, false);
328             }
329         });
330         step3.playTogether(animationList);
331 
332         mTabCreatedBackgroundAnimation = new AnimatorSet();
333         mTabCreatedBackgroundAnimation.playSequentially(step1, step3);
334         mTabCreatedBackgroundAnimation.start();
335 
336         mTabModelSelector.selectModel(newIsIncognito);
337     }
338 
339     /**
340      * Set up for the tab closing animation
341      */
342     @Override
onTabClosing(long time, int id)343     public void onTabClosing(long time, int id) {
344         reset();
345 
346         // Make sure any currently running animations can't influence tab if we are reusing it.
347         forceAnimationToFinish();
348 
349         // Create the {@link LayoutTab} for the tab before it is destroyed.
350         TabModel model = mTabModelSelector.getModelForTabId(id);
351         if (model != null) {
352             mClosedTab = createLayoutTab(id, model.isIncognito(), NO_CLOSE_BUTTON, NO_TITLE);
353             mClosedTab.setBorderAlpha(0.0f);
354             mLayoutTabs = new LayoutTab[] {mClosedTab};
355             updateCacheVisibleIds(new LinkedList<Integer>(Arrays.asList(id)));
356         } else {
357             mLayoutTabs = null;
358             mClosedTab = null;
359         }
360         // Only close the id at the end when we are done querying the model.
361         super.onTabClosing(time, id);
362     }
363 
364     /**
365      * Animate the closing of a tab
366      */
367     @Override
onTabClosed(long time, int id, int nextId, boolean incognito)368     public void onTabClosed(long time, int id, int nextId, boolean incognito) {
369         super.onTabClosed(time, id, nextId, incognito);
370 
371         if (mClosedTab != null) {
372             TabModel nextModel = mTabModelSelector.getModelForTabId(nextId);
373             if (nextModel != null) {
374                 LayoutTab nextLayoutTab =
375                         createLayoutTab(nextId, nextModel.isIncognito(), NO_CLOSE_BUTTON, NO_TITLE);
376                 nextLayoutTab.setDrawDecoration(false);
377 
378                 mLayoutTabs = new LayoutTab[] {nextLayoutTab, mClosedTab};
379                 updateCacheVisibleIds(
380                         new LinkedList<Integer>(Arrays.asList(nextId, mClosedTab.getId())));
381             } else {
382                 mLayoutTabs = new LayoutTab[] {mClosedTab};
383             }
384 
385             forceAnimationToFinish();
386             mAnimatedTab = mClosedTab;
387             mDiscardAnimator = CompositorAnimator.ofFloat(getAnimationHandler(), 0,
388                     getDiscardRange(), TAB_CLOSED_ANIMATION_DURATION,
389                     (CompositorAnimator a) -> setDiscardAmount(a.getAnimatedValue()));
390             mDiscardAnimator.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);
391             mDiscardAnimator.start();
392 
393             mClosedTab = null;
394             if (nextModel != null) {
395                 mTabModelSelector.selectModel(nextModel.isIncognito());
396             }
397         }
398         startHiding(nextId, false);
399     }
400 
401     /**
402      * Updates the position, scale, rotation and alpha values of mAnimatedTab.
403      *
404      * @param discard The value that specify how far along are we in the discard animation. 0 is
405      *                filling the screen. Valid values are [-range .. range] where range is
406      *                computed by {@link SimpleAnimationLayout#getDiscardRange()}.
407      */
setDiscardAmount(float discard)408     private void setDiscardAmount(float discard) {
409         if (mAnimatedTab != null) {
410             final float range = getDiscardRange();
411             final float scale = Stack.computeDiscardScale(discard, range, true);
412 
413             final float deltaX = mAnimatedTab.getOriginalContentWidth();
414             final float deltaY = mAnimatedTab.getOriginalContentHeight() / 2.f;
415             mAnimatedTab.setX(deltaX * (1.f - scale));
416             mAnimatedTab.setY(deltaY * (1.f - scale));
417             mAnimatedTab.setScale(scale);
418             mAnimatedTab.setBorderScale(scale);
419             mAnimatedTab.setAlpha(Stack.computeDiscardAlpha(discard, range));
420         }
421     }
422 
423     /**
424      * @return The range of the discard amount.
425      */
getDiscardRange()426     private float getDiscardRange() {
427         return Math.min(getWidth(), getHeight()) * Stack.DISCARD_RANGE_SCREEN;
428     }
429 
430     @Override
forceAnimationToFinish()431     protected void forceAnimationToFinish() {
432         super.forceAnimationToFinish();
433         if (mDiscardAnimator != null) mDiscardAnimator.end();
434         if (mTabCreatedForegroundAnimation != null) mTabCreatedForegroundAnimation.end();
435         if (mTabCreatedBackgroundAnimation != null) mTabCreatedBackgroundAnimation.end();
436     }
437 
438     /**
439      * Resets the internal state.
440      */
reset()441     private void reset() {
442         mLayoutTabs = null;
443         mAnimatedTab = null;
444         mClosedTab = null;
445     }
446 
447     @Override
getEventFilter()448     protected EventFilter getEventFilter() {
449         return mBlackHoleEventFilter;
450     }
451 
452     @Override
getSceneLayer()453     protected SceneLayer getSceneLayer() {
454         return mSceneLayer;
455     }
456 
457     @Override
updateSceneLayer(RectF viewport, RectF contentViewport, LayerTitleCache layerTitleCache, TabContentManager tabContentManager, ResourceManager resourceManager, BrowserControlsStateProvider browserControls)458     protected void updateSceneLayer(RectF viewport, RectF contentViewport,
459             LayerTitleCache layerTitleCache, TabContentManager tabContentManager,
460             ResourceManager resourceManager, BrowserControlsStateProvider browserControls) {
461         super.updateSceneLayer(viewport, contentViewport, layerTitleCache, tabContentManager,
462                 resourceManager, browserControls);
463         assert mSceneLayer != null;
464         // The content viewport is intentionally sent as both params below.
465         mSceneLayer.pushLayers(getContext(), contentViewport, contentViewport, this,
466                 layerTitleCache, tabContentManager, resourceManager, browserControls,
467                 SceneLayer.INVALID_RESOURCE_ID, 0, 0);
468     }
469 
470     @Override
getLayoutType()471     public int getLayoutType() {
472         return LayoutType.SIMPLE_ANIMATION;
473     }
474 }
475