// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.compositor.layouts.phone; import android.content.Context; import android.graphics.Rect; import android.view.animation.Interpolator; import org.chromium.chrome.browser.compositor.LayerTitleCache; import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animatable; import org.chromium.chrome.browser.compositor.layouts.Layout; import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost; import org.chromium.chrome.browser.compositor.layouts.LayoutUpdateHost; import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab; import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; import org.chromium.chrome.browser.compositor.layouts.eventfilter.EventFilter; import org.chromium.chrome.browser.compositor.layouts.phone.stack.Stack; import org.chromium.chrome.browser.compositor.layouts.phone.stack.StackAnimation; import org.chromium.chrome.browser.compositor.scene_layer.SceneLayer; import org.chromium.chrome.browser.compositor.scene_layer.TabListSceneLayer; import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tabmodel.TabModel; import org.chromium.ui.interpolators.BakedBezierInterpolator; import org.chromium.ui.resources.ResourceManager; import java.util.Arrays; import java.util.LinkedList; /** * This class handles animating the opening of new tabs. */ public class SimpleAnimationLayout extends Layout implements Animatable<SimpleAnimationLayout.Property> { /** * Animation properties */ public enum Property { DISCARD_AMOUNT } /** Duration of the first step of the background animation: zooming out, rotating in */ private static final long BACKGROUND_STEP1_DURATION = 300; /** Duration of the second step of the background animation: pause */ private static final long BACKGROUND_STEP2_DURATION = 150; /** Duration of the third step of the background animation: zooming in, sliding out */ private static final long BACKGROUND_STEP3_DURATION = 300; /** Start time offset of the third step of the background animation */ private static final long BACKGROUND_STEP3_START = BACKGROUND_STEP1_DURATION + BACKGROUND_STEP2_DURATION; /** Percentage of the screen covered by the new tab */ private static final float BACKGROUND_COVER_PCTG = 0.5f; /** The time duration of the animation */ protected static final int FOREGROUND_ANIMATION_DURATION = 300; /** The time duration of the animation */ protected static final int TAB_CLOSED_ANIMATION_DURATION = 250; /** * A cached {@link LayoutTab} representation of the currently closing tab. If it's not * null, it means tabClosing() has been called to start animation setup but * tabClosed() has not yet been called to finish animation startup */ private LayoutTab mClosedTab; private LayoutTab mAnimatedTab; private final TabListSceneLayer mSceneLayer; /** * Creates an instance of the {@link SimpleAnimationLayout}. * @param context The current Android's context. * @param updateHost The {@link LayoutUpdateHost} view for this layout. * @param renderHost The {@link LayoutRenderHost} view for this layout. * @param eventFilter The {@link EventFilter} that is needed for this view. */ public SimpleAnimationLayout(Context context, LayoutUpdateHost updateHost, LayoutRenderHost renderHost, EventFilter eventFilter) { super(context, updateHost, renderHost, eventFilter); mSceneLayer = new TabListSceneLayer(); } @Override public int getSizingFlags() { return SizingFlags.HELPER_SUPPORTS_FULLSCREEN; } @Override public void show(long time, boolean animate) { super.show(time, animate); if (mTabModelSelector != null && mTabContentManager != null) { Tab tab = mTabModelSelector.getCurrentTab(); if (tab != null && tab.isNativePage()) mTabContentManager.cacheTabThumbnail(tab); } reset(); } @Override public boolean handlesTabCreating() { return true; } @Override public boolean handlesTabClosing() { return true; } @Override protected void updateLayout(long time, long dt) { super.updateLayout(time, dt); if (mLayoutTabs == null) return; boolean needUpdate = false; for (int i = mLayoutTabs.length - 1; i >= 0; i--) { needUpdate = mLayoutTabs[i].updateSnap(dt) || needUpdate; } if (needUpdate) requestUpdate(); } @Override public void onTabCreating(int sourceTabId) { super.onTabCreating(sourceTabId); reset(); // Make sure any currently running animations can't influence tab if we are reusing it. forceAnimationToFinish(); ensureSourceTabCreated(sourceTabId); } private void ensureSourceTabCreated(int sourceTabId) { if (mLayoutTabs != null && mLayoutTabs.length == 1 && mLayoutTabs[0].getId() == sourceTabId) { return; } // Just draw the source tab on the screen. TabModel sourceModel = mTabModelSelector.getModelForTabId(sourceTabId); if (sourceModel == null) return; LayoutTab sourceLayoutTab = createLayoutTab(sourceTabId, sourceModel.isIncognito(), NO_CLOSE_BUTTON, NO_TITLE); sourceLayoutTab.setBorderAlpha(0.0f); mLayoutTabs = new LayoutTab[] {sourceLayoutTab}; updateCacheVisibleIds(new LinkedList<Integer>(Arrays.asList(sourceTabId))); } @Override public void onTabCreated(long time, int id, int index, int sourceId, boolean newIsIncognito, boolean background, float originX, float originY) { super.onTabCreated(time, id, index, sourceId, newIsIncognito, background, originX, originY); ensureSourceTabCreated(sourceId); if (background && mLayoutTabs != null && mLayoutTabs.length > 0) { tabCreatedInBackground(id, sourceId, newIsIncognito, originX, originY); } else { tabCreatedInForeground(id, sourceId, newIsIncognito, originX, originY); } } /** * Animate opening a tab in the foreground. * * @param id The id of the new tab to animate. * @param sourceId The id of the tab that spawned this new tab. * @param newIsIncognito true if the new tab is an incognito tab. * @param originX The X coordinate of the last touch down event that spawned this tab. * @param originY The Y coordinate of the last touch down event that spawned this tab. */ private void tabCreatedInForeground( int id, int sourceId, boolean newIsIncognito, float originX, float originY) { LayoutTab newLayoutTab = createLayoutTab(id, newIsIncognito, NO_CLOSE_BUTTON, NO_TITLE); if (mLayoutTabs == null || mLayoutTabs.length == 0) { mLayoutTabs = new LayoutTab[] {newLayoutTab}; } else { mLayoutTabs = new LayoutTab[] {mLayoutTabs[0], newLayoutTab}; } updateCacheVisibleIds(new LinkedList<Integer>(Arrays.asList(id, sourceId))); newLayoutTab.setBorderAlpha(0.0f); newLayoutTab.setStaticToViewBlend(1.f); forceAnimationToFinish(); Interpolator interpolator = BakedBezierInterpolator.TRANSFORM_CURVE; addToAnimation(newLayoutTab, LayoutTab.Property.SCALE, 0.f, 1.f, FOREGROUND_ANIMATION_DURATION, 0, false, interpolator); addToAnimation(newLayoutTab, LayoutTab.Property.ALPHA, 0.f, 1.f, FOREGROUND_ANIMATION_DURATION, 0, false, interpolator); addToAnimation(newLayoutTab, LayoutTab.Property.X, originX, 0.f, FOREGROUND_ANIMATION_DURATION, 0, false, interpolator); addToAnimation(newLayoutTab, LayoutTab.Property.Y, originY, 0.f, FOREGROUND_ANIMATION_DURATION, 0, false, interpolator); mTabModelSelector.selectModel(newIsIncognito); startHiding(id, false); } /** * Animate opening a tab in the background. * * @param id The id of the new tab to animate. * @param sourceId The id of the tab that spawned this new tab. * @param newIsIncognito true if the new tab is an incognito tab. * @param originX The X screen coordinate in dp of the last touch down event that spawned * this tab. * @param originY The Y screen coordinate in dp of the last touch down event that spawned * this tab. */ private void tabCreatedInBackground( int id, int sourceId, boolean newIsIncognito, float originX, float originY) { LayoutTab newLayoutTab = createLayoutTab(id, newIsIncognito, NO_CLOSE_BUTTON, NEED_TITLE); // mLayoutTabs should already have the source tab from tabCreating(). assert mLayoutTabs.length == 1; LayoutTab sourceLayoutTab = mLayoutTabs[0]; mLayoutTabs = new LayoutTab[] {sourceLayoutTab, newLayoutTab}; updateCacheVisibleIds(new LinkedList<Integer>(Arrays.asList(id, sourceId))); forceAnimationToFinish(); newLayoutTab.setBorderAlpha(0.0f); final float scale = StackAnimation.SCALE_AMOUNT; final float margin = Math.min(getWidth(), getHeight()) * (1.0f - scale) / 2.0f; // Step 1: zoom out the source tab and bring in the new tab addToAnimation(sourceLayoutTab, LayoutTab.Property.SCALE, 1.0f, scale, BACKGROUND_STEP1_DURATION, 0, false, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(sourceLayoutTab, LayoutTab.Property.X, 0.0f, margin, BACKGROUND_STEP1_DURATION, 0, false, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(sourceLayoutTab, LayoutTab.Property.Y, 0.0f, margin, BACKGROUND_STEP1_DURATION, 0, false, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(sourceLayoutTab, LayoutTab.Property.BORDER_SCALE, 1.0f / scale, 1.0f, BACKGROUND_STEP1_DURATION, 0, false, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(sourceLayoutTab, LayoutTab.Property.BORDER_ALPHA, 0.0f, 1.0f, BACKGROUND_STEP1_DURATION, 0, false, BakedBezierInterpolator.TRANSFORM_CURVE); float pauseX = margin; float pauseY = margin; if (getOrientation() == Orientation.PORTRAIT) { pauseY = BACKGROUND_COVER_PCTG * getHeight(); } else { pauseX = BACKGROUND_COVER_PCTG * getWidth(); } addToAnimation(newLayoutTab, LayoutTab.Property.ALPHA, 0.0f, 1.0f, BACKGROUND_STEP1_DURATION / 2, 0, false, BakedBezierInterpolator.FADE_IN_CURVE); addToAnimation(newLayoutTab, LayoutTab.Property.SCALE, 0.f, scale, BACKGROUND_STEP1_DURATION, 0, false, BakedBezierInterpolator.FADE_IN_CURVE); addToAnimation(newLayoutTab, LayoutTab.Property.X, originX, pauseX, BACKGROUND_STEP1_DURATION, 0, false, BakedBezierInterpolator.FADE_IN_CURVE); addToAnimation(newLayoutTab, LayoutTab.Property.Y, originY, pauseY, BACKGROUND_STEP1_DURATION, 0, false, BakedBezierInterpolator.FADE_IN_CURVE); // step 2: pause and admire the nice tabs // step 3: zoom in the source tab and slide down the new tab addToAnimation(sourceLayoutTab, LayoutTab.Property.SCALE, scale, 1.0f, BACKGROUND_STEP3_DURATION, BACKGROUND_STEP3_START, true, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(sourceLayoutTab, LayoutTab.Property.X, margin, 0.0f, BACKGROUND_STEP3_DURATION, BACKGROUND_STEP3_START, true, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(sourceLayoutTab, LayoutTab.Property.Y, margin, 0.0f, BACKGROUND_STEP3_DURATION, BACKGROUND_STEP3_START, true, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(sourceLayoutTab, LayoutTab.Property.BORDER_SCALE, 1.0f, 1.0f / scale, BACKGROUND_STEP3_DURATION, BACKGROUND_STEP3_START, true, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(sourceLayoutTab, LayoutTab.Property.BORDER_ALPHA, 1.0f, 0.0f, BACKGROUND_STEP3_DURATION, BACKGROUND_STEP3_START, true, BakedBezierInterpolator.TRANSFORM_CURVE); addToAnimation(newLayoutTab, LayoutTab.Property.ALPHA, 1.f, 0.f, BACKGROUND_STEP3_DURATION, BACKGROUND_STEP3_START, true, BakedBezierInterpolator.FADE_OUT_CURVE); if (getOrientation() == Orientation.PORTRAIT) { addToAnimation(newLayoutTab, LayoutTab.Property.Y, pauseY, getHeight(), BACKGROUND_STEP3_DURATION, BACKGROUND_STEP3_START, true, BakedBezierInterpolator.FADE_OUT_CURVE); } else { addToAnimation(newLayoutTab, LayoutTab.Property.X, pauseX, getWidth(), BACKGROUND_STEP3_DURATION, BACKGROUND_STEP3_START, true, BakedBezierInterpolator.FADE_OUT_CURVE); } mTabModelSelector.selectModel(newIsIncognito); startHiding(sourceId, false); } /** * Set up for the tab closing animation */ @Override public void onTabClosing(long time, int id) { reset(); // Make sure any currently running animations can't influence tab if we are reusing it. forceAnimationToFinish(); // Create the {@link LayoutTab} for the tab before it is destroyed. TabModel model = mTabModelSelector.getModelForTabId(id); if (model != null) { mClosedTab = createLayoutTab(id, model.isIncognito(), NO_CLOSE_BUTTON, NO_TITLE); mClosedTab.setBorderAlpha(0.0f); mLayoutTabs = new LayoutTab[] {mClosedTab}; updateCacheVisibleIds(new LinkedList<Integer>(Arrays.asList(id))); } else { mLayoutTabs = null; mClosedTab = null; } // Only close the id at the end when we are done querying the model. super.onTabClosing(time, id); } /** * Animate the closing of a tab */ @Override public void onTabClosed(long time, int id, int nextId, boolean incognito) { super.onTabClosed(time, id, nextId, incognito); if (mClosedTab != null) { TabModel nextModel = mTabModelSelector.getModelForTabId(nextId); if (nextModel != null) { LayoutTab nextLayoutTab = createLayoutTab(nextId, nextModel.isIncognito(), NO_CLOSE_BUTTON, NO_TITLE); nextLayoutTab.setDrawDecoration(false); mLayoutTabs = new LayoutTab[] {nextLayoutTab, mClosedTab}; updateCacheVisibleIds( new LinkedList<Integer>(Arrays.asList(nextId, mClosedTab.getId()))); } else { mLayoutTabs = new LayoutTab[] {mClosedTab}; } forceAnimationToFinish(); mAnimatedTab = mClosedTab; addToAnimation(this, Property.DISCARD_AMOUNT, 0, getDiscardRange(), TAB_CLOSED_ANIMATION_DURATION, 0, false, BakedBezierInterpolator.FADE_OUT_CURVE); mClosedTab = null; if (nextModel != null) { mTabModelSelector.selectModel(nextModel.isIncognito()); } } startHiding(nextId, false); } /** * Updates the position, scale, rotation and alpha values of mAnimatedTab. * * @param discard The value that specify how far along are we in the discard animation. 0 is * filling the screen. Valid values are [-range .. range] where range is * computed by {@link SimpleAnimationLayout#getDiscardRange()}. */ private void setDiscardAmount(float discard) { if (mAnimatedTab != null) { final float range = getDiscardRange(); final float scale = Stack.computeDiscardScale(discard, range, true); final float deltaX = mAnimatedTab.getOriginalContentWidth(); final float deltaY = mAnimatedTab.getOriginalContentHeight() / 2.f; mAnimatedTab.setX(deltaX * (1.f - scale)); mAnimatedTab.setY(deltaY * (1.f - scale)); mAnimatedTab.setScale(scale); mAnimatedTab.setBorderScale(scale); mAnimatedTab.setAlpha(Stack.computeDiscardAlpha(discard, range)); } } /** * @return The range of the discard amount. */ private float getDiscardRange() { return Math.min(getWidth(), getHeight()) * Stack.DISCARD_RANGE_SCREEN; } @Override public boolean onUpdateAnimation(long time, boolean jumpToEnd) { return super.onUpdateAnimation(time, jumpToEnd) && mClosedTab == null; } /** * Resets the internal state. */ private void reset() { mLayoutTabs = null; mAnimatedTab = null; mClosedTab = null; } /** * Sets a property for an animation. * @param prop The property to update * @param value New value of the property */ @Override public void setProperty(Property prop, float value) { switch (prop) { case DISCARD_AMOUNT: setDiscardAmount(value); break; default: } } @Override public void onPropertyAnimationFinished(Property prop) {} @Override protected SceneLayer getSceneLayer() { return mSceneLayer; } @Override protected void updateSceneLayer(Rect viewport, Rect contentViewport, LayerTitleCache layerTitleCache, TabContentManager tabContentManager, ResourceManager resourceManager, ChromeFullscreenManager fullscreenManager) { super.updateSceneLayer(viewport, contentViewport, layerTitleCache, tabContentManager, resourceManager, fullscreenManager); assert mSceneLayer != null; mSceneLayer.pushLayers(getContext(), viewport, contentViewport, this, layerTitleCache, tabContentManager, resourceManager); } }