/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.res.Resources; import android.util.Log; import android.view.View; import android.view.animation.AccelerateInterpolator; import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.anim.AnimationLayerSet; import com.android.launcher3.anim.CircleRevealOutlineProvider; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.Thunk; import com.android.launcher3.widget.WidgetsContainerView; /** * TODO: figure out what kind of tests we can write for this * * Things to test when changing the following class. * - Home from workspace * - from center screen * - from other screens * - Home from all apps * - from center screen * - from other screens * - Back from all apps * - from center screen * - from other screens * - Launch app from workspace and quit * - with back * - with home * - Launch app from all apps and quit * - with back * - with home * - Go to a screen that's not the default, then all * apps, and launch and app, and go back * - with back * -with home * - On workspace, long press power and go back * - with back * - with home * - On all apps, long press power and go back * - with back * - with home * - On workspace, power off * - On all apps, power off * - Launch an app and turn off the screen while in that app * - Go back with home key * - Go back with back key TODO: make this not go to workspace * - From all apps * - From workspace * - Enter and exit car mode (becuase it causes an extra configuration changed) * - From all apps * - From the center workspace * - From another workspace */ public class LauncherStateTransitionAnimation { /** * animation used for the widget tray */ public static final int CIRCULAR_REVEAL = 0; /** * animation used for all apps tray */ public static final int PULLUP = 1; private static final float FINAL_REVEAL_ALPHA_FOR_WIDGETS = 0.3f; /** * Private callbacks made during transition setup. */ private static class PrivateTransitionCallbacks { private final float materialRevealViewFinalAlpha; PrivateTransitionCallbacks(float revealAlpha) { materialRevealViewFinalAlpha = revealAlpha; } float getMaterialRevealViewStartFinalRadius() { return 0; } AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(View revealView, View buttonView) { return null; } void onTransitionComplete() {} } public static final String TAG = "LSTAnimation"; public static final int SINGLE_FRAME_DELAY = 16; @Thunk Launcher mLauncher; @Thunk AnimatorSet mCurrentAnimation; AllAppsTransitionController mAllAppsController; public LauncherStateTransitionAnimation(Launcher l, AllAppsTransitionController allAppsController) { mLauncher = l; mAllAppsController = allAppsController; } /** * Starts an animation to the apps view. */ public void startAnimationToAllApps(final boolean animated) { final AllAppsContainerView toView = mLauncher.getAppsView(); final View buttonView = mLauncher.getStartViewForAllAppsRevealAnimation(); PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks(1f) { @Override public float getMaterialRevealViewStartFinalRadius() { int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize; return allAppsButtonSize / 2; } @Override public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( final View revealView, final View allAppsButtonView) { return new AnimatorListenerAdapter() { public void onAnimationStart(Animator animation) { allAppsButtonView.setVisibility(View.INVISIBLE); } public void onAnimationEnd(Animator animation) { allAppsButtonView.setVisibility(View.VISIBLE); } }; } @Override void onTransitionComplete() { mLauncher.getUserEventDispatcher().resetElapsedContainerMillis(); } }; // Only animate the search bar if animating from spring loaded mode back to all apps startAnimationToOverlay( Workspace.State.NORMAL_HIDDEN, buttonView, toView, animated, PULLUP, cb); } /** * Starts an animation to the widgets view. */ public void startAnimationToWidgets(final boolean animated) { final WidgetsContainerView toView = mLauncher.getWidgetsView(); final View buttonView = mLauncher.getWidgetsButton(); startAnimationToOverlay( Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, animated, CIRCULAR_REVEAL, new PrivateTransitionCallbacks(FINAL_REVEAL_ALPHA_FOR_WIDGETS){ @Override void onTransitionComplete() { mLauncher.getUserEventDispatcher().resetElapsedContainerMillis(); } }); } /** * Starts an animation to the workspace from the current overlay view. */ public void startAnimationToWorkspace(final Launcher.State fromState, final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, final boolean animated, final Runnable onCompleteRunnable) { if (toWorkspaceState != Workspace.State.NORMAL && toWorkspaceState != Workspace.State.SPRING_LOADED && toWorkspaceState != Workspace.State.OVERVIEW) { Log.e(TAG, "Unexpected call to startAnimationToWorkspace"); } if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED || mAllAppsController.isTransitioning()) { startAnimationToWorkspaceFromAllApps(fromWorkspaceState, toWorkspaceState, animated, PULLUP, onCompleteRunnable); } else if (fromState == Launcher.State.WIDGETS || fromState == Launcher.State.WIDGETS_SPRING_LOADED) { startAnimationToWorkspaceFromWidgets(fromWorkspaceState, toWorkspaceState, animated, onCompleteRunnable); } else { startAnimationToNewWorkspaceState(fromWorkspaceState, toWorkspaceState, animated, onCompleteRunnable); } } /** * Creates and starts a new animation to a particular overlay view. */ private void startAnimationToOverlay( final Workspace.State toWorkspaceState, final View buttonView, final BaseContainerView toView, final boolean animated, int animType, final PrivateTransitionCallbacks pCb) { final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); final Resources res = mLauncher.getResources(); final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); final int revealDurationSlide = res.getInteger(R.integer.config_overlaySlideRevealTime); final int itemsAlphaStagger = res.getInteger(R.integer.config_overlayItemsAlphaStagger); final AnimationLayerSet layerViews = new AnimationLayerSet(); // If for some reason our views aren't initialized, don't animate boolean initialized = buttonView != null; // Cancel the current animation cancelAnimation(); final View contentView = toView.getContentView(); playCommonTransitionAnimations(toWorkspaceState, animated, initialized, animation, layerViews); if (!animated || !initialized) { if (toWorkspaceState == Workspace.State.NORMAL_HIDDEN) { mAllAppsController.finishPullUp(); } toView.setTranslationX(0.0f); toView.setTranslationY(0.0f); toView.setScaleX(1.0f); toView.setScaleY(1.0f); toView.setAlpha(1.0f); toView.setVisibility(View.VISIBLE); // Show the content view contentView.setVisibility(View.VISIBLE); pCb.onTransitionComplete(); return; } if (animType == CIRCULAR_REVEAL) { // Setup the reveal view animation final View revealView = toView.getRevealView(); int width = revealView.getMeasuredWidth(); int height = revealView.getMeasuredHeight(); float revealRadius = (float) Math.hypot(width / 2, height / 2); revealView.setVisibility(View.VISIBLE); revealView.setAlpha(0f); revealView.setTranslationY(0f); revealView.setTranslationX(0f); // Calculate the final animation values int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, buttonView); final float revealViewToAlpha = pCb.materialRevealViewFinalAlpha; final float revealViewToXDrift = buttonViewToPanelDelta[0]; final float revealViewToYDrift = buttonViewToPanelDelta[1]; // Create the animators PropertyValuesHolder panelAlpha = PropertyValuesHolder.ofFloat(View.ALPHA, revealViewToAlpha, 1f); PropertyValuesHolder panelDriftY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, revealViewToYDrift, 0); PropertyValuesHolder panelDriftX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, revealViewToXDrift, 0); ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView, panelAlpha, panelDriftY, panelDriftX); panelAlphaAndDrift.setDuration(revealDuration); panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); // Play the animation layerViews.addView(revealView); animation.play(panelAlphaAndDrift); // Setup the animation for the content view contentView.setVisibility(View.VISIBLE); contentView.setAlpha(0f); contentView.setTranslationY(revealViewToYDrift); layerViews.addView(contentView); // Create the individual animators ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY", revealViewToYDrift, 0); pageDrift.setDuration(revealDuration); pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); pageDrift.setStartDelay(itemsAlphaStagger); animation.play(pageDrift); ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f); itemsAlpha.setDuration(revealDuration); itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); itemsAlpha.setStartDelay(itemsAlphaStagger); animation.play(itemsAlpha); float startRadius = pCb.getMaterialRevealViewStartFinalRadius(); AnimatorListenerAdapter listener = pCb.getMaterialRevealViewAnimatorListener( revealView, buttonView); Animator reveal = new CircleRevealOutlineProvider(width / 2, height / 2, startRadius, revealRadius).createRevealAnimator(revealView); reveal.setDuration(revealDuration); reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); if (listener != null) { reveal.addListener(listener); } animation.play(reveal); animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // Hide the reveal view revealView.setVisibility(View.INVISIBLE); // This can hold unnecessary references to views. cleanupAnimation(); pCb.onTransitionComplete(); } }); toView.bringToFront(); toView.setVisibility(View.VISIBLE); animation.addListener(layerViews); toView.post(new StartAnimRunnable(animation, toView)); mCurrentAnimation = animation; } else if (animType == PULLUP) { if (!FeatureFlags.LAUNCHER3_PHYSICS) { // We are animating the content view alpha, so ensure we have a layer for it. layerViews.addView(contentView); } animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { cleanupAnimation(); pCb.onTransitionComplete(); } }); boolean shouldPost = mAllAppsController.animateToAllApps(animation, revealDurationSlide); Runnable startAnimRunnable = new StartAnimRunnable(animation, toView); mCurrentAnimation = animation; mCurrentAnimation.addListener(layerViews); if (shouldPost) { toView.post(startAnimRunnable); } else { startAnimRunnable.run(); } } } /** * Plays animations used by various transitions. */ private void playCommonTransitionAnimations( Workspace.State toWorkspaceState, boolean animated, boolean initialized, AnimatorSet animation, AnimationLayerSet layerViews) { // Create the workspace animation. // NOTE: this call apparently also sets the state for the workspace if !animated Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState, animated, layerViews); if (animated && initialized) { // Play the workspace animation if (workspaceAnim != null) { animation.play(workspaceAnim); } } } /** * Starts an animation to the workspace from the apps view. */ private void startAnimationToWorkspaceFromAllApps(final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, final boolean animated, int type, final Runnable onCompleteRunnable) { // No alpha anim from all apps PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks(1f) { @Override float getMaterialRevealViewStartFinalRadius() { int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize; return allAppsButtonSize / 2; } @Override public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( final View revealView, final View allAppsButtonView) { return new AnimatorListenerAdapter() { public void onAnimationStart(Animator animation) { // We set the alpha instead of visibility to ensure that the focus does not // get taken from the all apps view allAppsButtonView.setVisibility(View.VISIBLE); allAppsButtonView.setAlpha(0f); } public void onAnimationEnd(Animator animation) { // Hide the reveal view revealView.setVisibility(View.INVISIBLE); // Show the all apps button, and focus it allAppsButtonView.setAlpha(1f); } }; } @Override void onTransitionComplete() { mLauncher.getUserEventDispatcher().resetElapsedContainerMillis(); } }; // Only animate the search bar if animating to spring loaded mode from all apps startAnimationToWorkspaceFromOverlay(fromWorkspaceState, toWorkspaceState, mLauncher.getStartViewForAllAppsRevealAnimation(), mLauncher.getAppsView(), animated, type, onCompleteRunnable, cb); } /** * Starts an animation to the workspace from the widgets view. */ private void startAnimationToWorkspaceFromWidgets(final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, final boolean animated, final Runnable onCompleteRunnable) { final WidgetsContainerView widgetsView = mLauncher.getWidgetsView(); PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks(FINAL_REVEAL_ALPHA_FOR_WIDGETS) { @Override public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( final View revealView, final View widgetsButtonView) { return new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { // Hide the reveal view revealView.setVisibility(View.INVISIBLE); } }; } @Override void onTransitionComplete() { mLauncher.getUserEventDispatcher().resetElapsedContainerMillis(); } }; startAnimationToWorkspaceFromOverlay( fromWorkspaceState, toWorkspaceState, mLauncher.getWidgetsButton(), widgetsView, animated, CIRCULAR_REVEAL, onCompleteRunnable, cb); } /** * Starts an animation to the workspace from another workspace state, e.g. normal to overview. */ private void startAnimationToNewWorkspaceState(final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, final boolean animated, final Runnable onCompleteRunnable) { final View fromWorkspace = mLauncher.getWorkspace(); final AnimationLayerSet layerViews = new AnimationLayerSet(); final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); // Cancel the current animation cancelAnimation(); playCommonTransitionAnimations(toWorkspaceState, animated, animated, animation, layerViews); mLauncher.getUserEventDispatcher().resetElapsedContainerMillis(); if (animated) { animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // Run any queued runnables if (onCompleteRunnable != null) { onCompleteRunnable.run(); } // This can hold unnecessary references to views. cleanupAnimation(); } }); animation.addListener(layerViews); fromWorkspace.post(new StartAnimRunnable(animation, null)); mCurrentAnimation = animation; } else /* if (!animated) */ { // Run any queued runnables if (onCompleteRunnable != null) { onCompleteRunnable.run(); } mCurrentAnimation = null; } } /** * Creates and starts a new animation to the workspace. */ private void startAnimationToWorkspaceFromOverlay( final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, final View buttonView, final BaseContainerView fromView, final boolean animated, int animType, final Runnable onCompleteRunnable, final PrivateTransitionCallbacks pCb) { final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); final Resources res = mLauncher.getResources(); final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); final int revealDurationSlide = res.getInteger(R.integer.config_overlaySlideRevealTime); final int itemsAlphaStagger = res.getInteger(R.integer.config_overlayItemsAlphaStagger); final View toView = mLauncher.getWorkspace(); final View revealView = fromView.getRevealView(); final View contentView = fromView.getContentView(); final AnimationLayerSet layerViews = new AnimationLayerSet(); // If for some reason our views aren't initialized, don't animate boolean initialized = buttonView != null; // Cancel the current animation cancelAnimation(); playCommonTransitionAnimations(toWorkspaceState, animated, initialized, animation, layerViews); if (!animated || !initialized) { if (fromWorkspaceState == Workspace.State.NORMAL_HIDDEN) { mAllAppsController.finishPullDown(); } fromView.setVisibility(View.GONE); pCb.onTransitionComplete(); // Run any queued runnables if (onCompleteRunnable != null) { onCompleteRunnable.run(); } return; } if (animType == CIRCULAR_REVEAL) { // hideAppsCustomizeHelper is called in some cases when it is already hidden // don't perform all these no-op animations. In particularly, this was causing // the all-apps button to pop in and out. if (fromView.getVisibility() == View.VISIBLE) { int width = revealView.getMeasuredWidth(); int height = revealView.getMeasuredHeight(); float revealRadius = (float) Math.hypot(width / 2, height / 2); revealView.setVisibility(View.VISIBLE); revealView.setAlpha(1f); revealView.setTranslationY(0); layerViews.addView(revealView); // Calculate the final animation values int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, buttonView); final float revealViewToXDrift = buttonViewToPanelDelta[0]; final float revealViewToYDrift = buttonViewToPanelDelta[1]; // The vertical motion of the apps panel should be delayed by one frame // from the conceal animation in order to give the right feel. We correspondingly // shorten the duration so that the slide and conceal end at the same time. TimeInterpolator decelerateInterpolator = new LogDecelerateInterpolator(100, 0); ObjectAnimator panelDriftY = ObjectAnimator.ofFloat(revealView, "translationY", 0, revealViewToYDrift); panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY); panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); panelDriftY.setInterpolator(decelerateInterpolator); animation.play(panelDriftY); ObjectAnimator panelDriftX = ObjectAnimator.ofFloat(revealView, "translationX", 0, revealViewToXDrift); panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY); panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); panelDriftX.setInterpolator(decelerateInterpolator); animation.play(panelDriftX); // Setup animation for the reveal panel alpha if (pCb.materialRevealViewFinalAlpha != 1f) { ObjectAnimator panelAlpha = ObjectAnimator.ofFloat(revealView, "alpha", 1f, pCb.materialRevealViewFinalAlpha); panelAlpha.setDuration(revealDuration); panelAlpha.setInterpolator(decelerateInterpolator); animation.play(panelAlpha); } // Setup the animation for the content view layerViews.addView(contentView); // Create the individual animators ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY", 0, revealViewToYDrift); contentView.setTranslationY(0); pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY); pageDrift.setInterpolator(decelerateInterpolator); pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); animation.play(pageDrift); contentView.setAlpha(1f); ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 1f, 0f); itemsAlpha.setDuration(100); itemsAlpha.setInterpolator(decelerateInterpolator); animation.play(itemsAlpha); // Invalidate the scrim throughout the animation to ensure the highlight // cutout is correct throughout. ValueAnimator invalidateScrim = ValueAnimator.ofFloat(0f, 1f); invalidateScrim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mLauncher.getDragLayer().invalidateScrim(); } }); animation.play(invalidateScrim); // Animate the all apps button float finalRadius = pCb.getMaterialRevealViewStartFinalRadius(); AnimatorListenerAdapter listener = pCb.getMaterialRevealViewAnimatorListener(revealView, buttonView); Animator reveal = new CircleRevealOutlineProvider(width / 2, height / 2, revealRadius, finalRadius).createRevealAnimator(revealView); reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); reveal.setDuration(revealDuration); reveal.setStartDelay(itemsAlphaStagger); if (listener != null) { reveal.addListener(listener); } animation.play(reveal); } animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { fromView.setVisibility(View.GONE); // Run any queued runnables if (onCompleteRunnable != null) { onCompleteRunnable.run(); } // Reset page transforms if (contentView != null) { contentView.setTranslationX(0); contentView.setTranslationY(0); contentView.setAlpha(1); } // This can hold unnecessary references to views. cleanupAnimation(); pCb.onTransitionComplete(); } }); mCurrentAnimation = animation; mCurrentAnimation.addListener(layerViews); fromView.post(new StartAnimRunnable(animation, null)); } else if (animType == PULLUP) { // We are animating the content view alpha, so ensure we have a layer for it layerViews.addView(contentView); animation.addListener(new AnimatorListenerAdapter() { boolean canceled = false; @Override public void onAnimationCancel(Animator animation) { canceled = true; } @Override public void onAnimationEnd(Animator animation) { if (canceled) return; // Run any queued runnables if (onCompleteRunnable != null) { onCompleteRunnable.run(); } cleanupAnimation(); pCb.onTransitionComplete(); } }); boolean shouldPost = mAllAppsController.animateToWorkspace(animation, revealDurationSlide); Runnable startAnimRunnable = new StartAnimRunnable(animation, toView); mCurrentAnimation = animation; mCurrentAnimation.addListener(layerViews); if (shouldPost) { fromView.post(startAnimRunnable); } else { startAnimRunnable.run(); } } return; } /** * Cancels the current animation. */ private void cancelAnimation() { if (mCurrentAnimation != null) { mCurrentAnimation.setDuration(0); mCurrentAnimation.cancel(); mCurrentAnimation = null; } } @Thunk void cleanupAnimation() { mCurrentAnimation = null; } private class StartAnimRunnable implements Runnable { private final AnimatorSet mAnim; private final View mViewToFocus; public StartAnimRunnable(AnimatorSet anim, View viewToFocus) { mAnim = anim; mViewToFocus = viewToFocus; } @Override public void run() { if (mCurrentAnimation != mAnim) { return; } if (mViewToFocus != null) { mViewToFocus.requestFocus(); } mAnim.start(); } } }