/* * 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.TimeInterpolator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; import android.view.View; import android.view.accessibility.AccessibilityManager; import android.view.animation.DecelerateInterpolator; import com.android.launcher3.util.Thunk; import java.util.HashMap; /** * A convenience class to update a view's visibility state after an alpha animation. */ class AlphaUpdateListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener { private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f; private View mView; private boolean mAccessibilityEnabled; public AlphaUpdateListener(View v, boolean accessibilityEnabled) { mView = v; mAccessibilityEnabled = accessibilityEnabled; } @Override public void onAnimationUpdate(ValueAnimator arg0) { updateVisibility(mView, mAccessibilityEnabled); } public static void updateVisibility(View view, boolean accessibilityEnabled) { // We want to avoid the extra layout pass by setting the views to GONE unless // accessibility is on, in which case not setting them to GONE causes a glitch. int invisibleState = accessibilityEnabled ? View.GONE : View.INVISIBLE; if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) { view.setVisibility(invisibleState); } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != View.VISIBLE) { view.setVisibility(View.VISIBLE); } } @Override public void onAnimationEnd(Animator arg0) { updateVisibility(mView, mAccessibilityEnabled); } @Override public void onAnimationStart(Animator arg0) { // We want the views to be visible for animation, so fade-in/out is visible mView.setVisibility(View.VISIBLE); } } /** * This interpolator emulates the rate at which the perceived scale of an object changes * as its distance from a camera increases. When this interpolator is applied to a scale * animation on a view, it evokes the sense that the object is shrinking due to moving away * from the camera. */ class ZInterpolator implements TimeInterpolator { private float focalLength; public ZInterpolator(float foc) { focalLength = foc; } public float getInterpolation(float input) { return (1.0f - focalLength / (focalLength + input)) / (1.0f - focalLength / (focalLength + 1.0f)); } } /** * The exact reverse of ZInterpolator. */ class InverseZInterpolator implements TimeInterpolator { private ZInterpolator zInterpolator; public InverseZInterpolator(float foc) { zInterpolator = new ZInterpolator(foc); } public float getInterpolation(float input) { return 1 - zInterpolator.getInterpolation(1 - input); } } /** * InverseZInterpolator compounded with an ease-out. */ class ZoomInInterpolator implements TimeInterpolator { private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f); private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f); public float getInterpolation(float input) { return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input)); } } /** * Manages the animations between each of the workspace states. */ public class WorkspaceStateTransitionAnimation { public static final String TAG = "WorkspaceStateTransitionAnimation"; public static final int SCROLL_TO_CURRENT_PAGE = -1; @Thunk static final int BACKGROUND_FADE_OUT_DURATION = 350; final @Thunk Launcher mLauncher; final @Thunk Workspace mWorkspace; @Thunk AnimatorSet mStateAnimator; @Thunk float[] mOldBackgroundAlphas; @Thunk float[] mOldAlphas; @Thunk float[] mNewBackgroundAlphas; @Thunk float[] mNewAlphas; @Thunk int mLastChildCount = -1; @Thunk float mCurrentScale; @Thunk float mNewScale; @Thunk final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator(); // These properties refer to the background protection gradient used for AllApps and Customize @Thunk ValueAnimator mBackgroundFadeInAnimation; @Thunk ValueAnimator mBackgroundFadeOutAnimation; @Thunk float mSpringLoadedShrinkFactor; @Thunk float mOverviewModeShrinkFactor; @Thunk float mWorkspaceScrimAlpha; @Thunk int mAllAppsTransitionTime; @Thunk int mOverviewTransitionTime; @Thunk int mOverlayTransitionTime; @Thunk boolean mWorkspaceFadeInAdjacentScreens; public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) { mLauncher = launcher; mWorkspace = workspace; LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); Resources res = launcher.getResources(); mAllAppsTransitionTime = res.getInteger(R.integer.config_workspaceUnshrinkTime); mOverviewTransitionTime = res.getInteger(R.integer.config_overviewTransitionTime); mOverlayTransitionTime = res.getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime); mSpringLoadedShrinkFactor = res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100f; mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f; mOverviewModeShrinkFactor = grid.getOverviewModeScale(); mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens(); } public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState, int toPage, boolean animated, HashMap layerViews) { getAnimation(fromState, toState, toPage, animated, layerViews); return mStateAnimator; } public float getFinalScale() { return mNewScale; } /** * Starts a transition animation for the workspace. */ private void getAnimation(final Workspace.State fromState, final Workspace.State toState, int toPage, final boolean animated, final HashMap layerViews) { AccessibilityManager am = (AccessibilityManager) mLauncher.getSystemService(Context.ACCESSIBILITY_SERVICE); boolean accessibilityEnabled = am.isEnabled(); // Reinitialize animation arrays for the current workspace state reinitializeAnimationArrays(); // Cancel existing workspace animations and create a new animator set if requested cancelAnimation(); if (animated) { mStateAnimator = LauncherAnimUtils.createAnimatorSet(); } // Update the workspace state final boolean oldStateIsNormal = (fromState == Workspace.State.NORMAL); final boolean oldStateIsSpringLoaded = (fromState == Workspace.State.SPRING_LOADED); final boolean oldStateIsNormalHidden = (fromState == Workspace.State.NORMAL_HIDDEN); final boolean oldStateIsOverviewHidden = (fromState == Workspace.State.OVERVIEW_HIDDEN); final boolean oldStateIsOverview = (fromState == Workspace.State.OVERVIEW); final boolean stateIsNormal = (toState == Workspace.State.NORMAL); final boolean stateIsSpringLoaded = (toState == Workspace.State.SPRING_LOADED); final boolean stateIsNormalHidden = (toState == Workspace.State.NORMAL_HIDDEN); final boolean stateIsOverviewHidden = (toState == Workspace.State.OVERVIEW_HIDDEN); final boolean stateIsOverview = (toState == Workspace.State.OVERVIEW); final boolean workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden); final boolean overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden); final boolean allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal); final boolean workspaceToOverview = (oldStateIsNormal && stateIsOverview); final boolean overviewToWorkspace = (oldStateIsOverview && stateIsNormal); float finalBackgroundAlpha = (stateIsSpringLoaded || stateIsOverview) ? 1.0f : 0f; float finalHotseatAndPageIndicatorAlpha = (stateIsNormal || stateIsSpringLoaded) ? 1f : 0f; float finalOverviewPanelAlpha = stateIsOverview ? 1f : 0f; // We keep the search bar visible on the workspace and in AllApps now boolean showSearchBar = stateIsNormal || (mLauncher.isAllAppsSearchOverridden() && stateIsNormalHidden); float finalSearchBarAlpha = showSearchBar ? 1f : 0f; float finalWorkspaceTranslationY = stateIsOverview || stateIsOverviewHidden ? mWorkspace.getOverviewModeTranslationY() : 0; final int childCount = mWorkspace.getChildCount(); final int customPageCount = mWorkspace.numCustomPages(); mNewScale = 1.0f; if (oldStateIsOverview) { mWorkspace.disableFreeScroll(); } else if (stateIsOverview) { mWorkspace.enableFreeScroll(); } if (!stateIsNormal) { if (stateIsSpringLoaded) { mNewScale = mSpringLoadedShrinkFactor; } else if (stateIsOverview || stateIsOverviewHidden) { mNewScale = mOverviewModeShrinkFactor; } } final int duration; if (workspaceToAllApps || overviewToAllApps) { duration = mAllAppsTransitionTime; } else if (workspaceToOverview || overviewToWorkspace) { duration = mOverviewTransitionTime; } else { duration = mOverlayTransitionTime; } if (toPage == SCROLL_TO_CURRENT_PAGE) { toPage = mWorkspace.getPageNearestToCenterOfScreen(); } mWorkspace.snapToPage(toPage, duration, mZoomInInterpolator); for (int i = 0; i < childCount; i++) { final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); boolean isCurrentPage = (i == toPage); float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); float finalAlpha; if (stateIsNormalHidden || stateIsOverviewHidden) { finalAlpha = 0f; } else if (stateIsNormal && mWorkspaceFadeInAdjacentScreens) { finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f; } else { finalAlpha = 1f; } // If we are animating to/from the small state, then hide the side pages and fade the // current page in if (!mWorkspace.isSwitchingState()) { if (workspaceToAllApps || allAppsToWorkspace) { if (allAppsToWorkspace && isCurrentPage) { initialAlpha = 0f; } else if (!isCurrentPage) { initialAlpha = finalAlpha = 0f; } cl.setShortcutAndWidgetAlpha(initialAlpha); } } mOldAlphas[i] = initialAlpha; mNewAlphas[i] = finalAlpha; if (animated) { mOldBackgroundAlphas[i] = cl.getBackgroundAlpha(); mNewBackgroundAlphas[i] = finalBackgroundAlpha; } else { cl.setBackgroundAlpha(finalBackgroundAlpha); cl.setShortcutAndWidgetAlpha(finalAlpha); } } final View searchBar = mLauncher.getOrCreateQsbBar(); final View overviewPanel = mLauncher.getOverviewPanel(); final View hotseat = mLauncher.getHotseat(); final View pageIndicator = mWorkspace.getPageIndicator(); if (animated) { LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mWorkspace); scale.scaleX(mNewScale) .scaleY(mNewScale) .translationY(finalWorkspaceTranslationY) .setDuration(duration) .setInterpolator(mZoomInInterpolator); mStateAnimator.play(scale); for (int index = 0; index < childCount; index++) { final int i = index; final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); float currentAlpha = cl.getShortcutsAndWidgets().getAlpha(); if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) { cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); } else { if (layerViews != null) { layerViews.put(cl, LauncherStateTransitionAnimation.BUILD_LAYER); } if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) { LauncherViewPropertyAnimator alphaAnim = new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets()); alphaAnim.alpha(mNewAlphas[i]) .setDuration(duration) .setInterpolator(mZoomInInterpolator); mStateAnimator.play(alphaAnim); } if (mOldBackgroundAlphas[i] != 0 || mNewBackgroundAlphas[i] != 0) { ValueAnimator bgAnim = LauncherAnimUtils.ofFloat(cl, 0f, 1f); bgAnim.setInterpolator(mZoomInInterpolator); bgAnim.setDuration(duration); bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { public void onAnimationUpdate(float a, float b) { cl.setBackgroundAlpha( a * mOldBackgroundAlphas[i] + b * mNewBackgroundAlphas[i]); } }); mStateAnimator.play(bgAnim); } } } Animator pageIndicatorAlpha = null; if (pageIndicator != null) { pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator) .alpha(finalHotseatAndPageIndicatorAlpha).withLayer(); pageIndicatorAlpha.addListener(new AlphaUpdateListener(pageIndicator, accessibilityEnabled)); } else { // create a dummy animation so we don't need to do null checks later pageIndicatorAlpha = ValueAnimator.ofFloat(0, 0); } LauncherViewPropertyAnimator hotseatAlpha = new LauncherViewPropertyAnimator(hotseat) .alpha(finalHotseatAndPageIndicatorAlpha); hotseatAlpha.addListener(new AlphaUpdateListener(hotseat, accessibilityEnabled)); LauncherViewPropertyAnimator overviewPanelAlpha = new LauncherViewPropertyAnimator(overviewPanel).alpha(finalOverviewPanelAlpha); overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel, accessibilityEnabled)); // For animation optimations, we may need to provide the Launcher transition // with a set of views on which to force build layers in certain scenarios. hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null); overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null); if (layerViews != null) { // If layerViews is not null, we add these views, and indicate that // the caller can manage layer state. layerViews.put(hotseat, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); } else { // Otherwise let the animator handle layer management. hotseatAlpha.withLayer(); overviewPanelAlpha.withLayer(); } if (workspaceToOverview) { pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2)); hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); overviewPanelAlpha.setInterpolator(null); } else if (overviewToWorkspace) { pageIndicatorAlpha.setInterpolator(null); hotseatAlpha.setInterpolator(null); overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); } overviewPanelAlpha.setDuration(duration); pageIndicatorAlpha.setDuration(duration); hotseatAlpha.setDuration(duration); // TODO: This should really be coordinated with the SearchDropTargetBar, otherwise the // bar has no idea that it is hidden, and this has no idea what state the bar is // actually in. if (searchBar != null) { LauncherViewPropertyAnimator searchBarAlpha = new LauncherViewPropertyAnimator(searchBar) .alpha(finalSearchBarAlpha); searchBarAlpha.addListener(new AlphaUpdateListener(searchBar, accessibilityEnabled)); searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); if (layerViews != null) { // If layerViews is not null, we add these views, and indicate that // the caller can manage layer state. layerViews.put(searchBar, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); } else { // Otherwise let the animator handle layer management. searchBarAlpha.withLayer(); } searchBarAlpha.setDuration(duration); mStateAnimator.play(searchBarAlpha); } mStateAnimator.play(overviewPanelAlpha); mStateAnimator.play(hotseatAlpha); mStateAnimator.play(pageIndicatorAlpha); mStateAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mStateAnimator = null; } }); } else { overviewPanel.setAlpha(finalOverviewPanelAlpha); AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled); hotseat.setAlpha(finalHotseatAndPageIndicatorAlpha); AlphaUpdateListener.updateVisibility(hotseat, accessibilityEnabled); if (pageIndicator != null) { pageIndicator.setAlpha(finalHotseatAndPageIndicatorAlpha); AlphaUpdateListener.updateVisibility(pageIndicator, accessibilityEnabled); } if (searchBar != null) { searchBar.setAlpha(finalSearchBarAlpha); AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); } mWorkspace.updateCustomContentVisibility(); mWorkspace.setScaleX(mNewScale); mWorkspace.setScaleY(mNewScale); mWorkspace.setTranslationY(finalWorkspaceTranslationY); } if (stateIsNormal) { animateBackgroundGradient(0f, animated); } else { animateBackgroundGradient(mWorkspaceScrimAlpha, animated); } } /** * Reinitializes the arrays that we need for the animations on each page. */ private void reinitializeAnimationArrays() { final int childCount = mWorkspace.getChildCount(); if (mLastChildCount == childCount) return; mOldBackgroundAlphas = new float[childCount]; mOldAlphas = new float[childCount]; mNewBackgroundAlphas = new float[childCount]; mNewAlphas = new float[childCount]; } /** * Animates the background scrim. * TODO(winsonc): Is there a better place for this? * * @param finalAlpha the final alpha for the background scrim * @param animated whether or not to set the background alpha immediately */ private void animateBackgroundGradient(float finalAlpha, boolean animated) { // Cancel any running background animations cancelAnimator(mBackgroundFadeInAnimation); cancelAnimator(mBackgroundFadeOutAnimation); final DragLayer dragLayer = mLauncher.getDragLayer(); final float startAlpha = dragLayer.getBackgroundAlpha(); if (finalAlpha != startAlpha) { if (animated) { mBackgroundFadeOutAnimation = LauncherAnimUtils.ofFloat(mWorkspace, startAlpha, finalAlpha); mBackgroundFadeOutAnimation.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { dragLayer.setBackgroundAlpha( ((Float)animation.getAnimatedValue()).floatValue()); } }); mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION); mBackgroundFadeOutAnimation.start(); } else { dragLayer.setBackgroundAlpha(finalAlpha); } } } /** * Cancels the current animation. */ private void cancelAnimation() { cancelAnimator(mStateAnimator); mStateAnimator = null; } /** * Cancels the specified animation. */ private void cancelAnimator(Animator animator) { if (animator != null) { animator.setDuration(0); animator.cancel(); } } }