/* * Copyright (C) 2016 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.ObjectAnimator; import android.animation.ValueAnimator; import android.util.Log; import android.view.View; import android.view.animation.LinearInterpolator; import com.android.launcher3.anim.AnimationLayerSet; import com.android.launcher3.userevent.nano.LauncherLogProto.Action; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import static com.android.launcher3.Workspace.State.NORMAL; import static com.android.launcher3.Workspace.State.OVERVIEW; /** * Manages the animations that play as the user pinches to/from overview mode. * * It will look like this pinching in: * - Workspace scales down * - At some threshold 1, hotseat and QSB fade out (full animation) * - At a later threshold 2, panel buttons fade in and scrim fades in * - At a final threshold 3, snap to overview * * Pinching out: * - Workspace scales up * - At threshold 1, panel buttons fade out * - At threshold 2, hotseat and QSB fade in and scrim fades out * - At threshold 3, snap to workspace * * @see PinchToOverviewListener * @see PinchThresholdManager */ public class PinchAnimationManager { private static final String TAG = "PinchAnimationManager"; private static final int THRESHOLD_ANIM_DURATION = 150; private static final LinearInterpolator INTERPOLATOR = new LinearInterpolator(); private static final int INDEX_HOTSEAT = 0; private static final int INDEX_OVERVIEW_PANEL_BUTTONS = 1; private static final int INDEX_SCRIM = 2; private final Animator[] mAnimators = new Animator[3]; private Launcher mLauncher; private Workspace mWorkspace; private float mOverviewScale; private float mOverviewTranslationY; private int mNormalOverviewTransitionDuration; private boolean mIsAnimating; public PinchAnimationManager(Launcher launcher) { mLauncher = launcher; mWorkspace = launcher.mWorkspace; mOverviewScale = mWorkspace.getOverviewModeShrinkFactor(); mOverviewTranslationY = mWorkspace.getOverviewModeTranslationY(); mNormalOverviewTransitionDuration = mWorkspace.getStateTransitionAnimation() .mOverviewTransitionTime; } public int getNormalOverviewTransitionDuration() { return mNormalOverviewTransitionDuration; } /** * Interpolate from {@param currentProgress} to {@param toProgress}, calling * {@link #setAnimationProgress(float)} throughout the duration. If duration is -1, * the default overview transition duration is used. */ public void animateToProgress(float currentProgress, float toProgress, int duration, final PinchThresholdManager thresholdManager) { if (duration == -1) { duration = mNormalOverviewTransitionDuration; } ValueAnimator animator = ValueAnimator.ofFloat(currentProgress, toProgress); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float pinchProgress = (Float) animation.getAnimatedValue(); setAnimationProgress(pinchProgress); thresholdManager.updateAndAnimatePassedThreshold(pinchProgress, PinchAnimationManager.this); } } ); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mIsAnimating = false; thresholdManager.reset(); mWorkspace.onEndStateTransition(); } }); animator.setDuration(duration).start(); mIsAnimating = true; } public boolean isAnimating() { return mIsAnimating; } /** * Animates to the specified progress. This should be called repeatedly throughout the pinch * gesture to run animations that interpolate throughout the gesture. * @param interpolatedProgress The progress from 0 to 1, where 0 is overview and 1 is workspace. */ public void setAnimationProgress(float interpolatedProgress) { float interpolatedScale = interpolatedProgress * (1f - mOverviewScale) + mOverviewScale; float interpolatedTranslationY = (1f - interpolatedProgress) * mOverviewTranslationY; mWorkspace.setScaleX(interpolatedScale); mWorkspace.setScaleY(interpolatedScale); mWorkspace.setTranslationY(interpolatedTranslationY); setOverviewPanelsAlpha(1f - interpolatedProgress, 0); } /** * Animates certain properties based on which threshold was passed, and in what direction. The * starting state must also be taken into account because the thresholds mean different things * when going from workspace to overview and vice versa. * @param threshold One of {@link PinchThresholdManager#THRESHOLD_ONE}, * {@link PinchThresholdManager#THRESHOLD_TWO}, or * {@link PinchThresholdManager#THRESHOLD_THREE} * @param startState {@link Workspace.State#NORMAL} or {@link Workspace.State#OVERVIEW}. * @param goingTowards {@link Workspace.State#NORMAL} or {@link Workspace.State#OVERVIEW}. * Note that this doesn't have to be the opposite of startState; */ public void animateThreshold(float threshold, Workspace.State startState, Workspace.State goingTowards) { if (threshold == PinchThresholdManager.THRESHOLD_ONE) { if (startState == OVERVIEW) { animateOverviewPanelButtons(goingTowards == OVERVIEW); } else if (startState == NORMAL) { animateHotseatAndQsb(goingTowards == NORMAL); } } else if (threshold == PinchThresholdManager.THRESHOLD_TWO) { if (startState == OVERVIEW) { animateHotseatAndQsb(goingTowards == NORMAL); animateScrim(goingTowards == OVERVIEW); } else if (startState == NORMAL) { animateOverviewPanelButtons(goingTowards == OVERVIEW); animateScrim(goingTowards == OVERVIEW); } } else if (threshold == PinchThresholdManager.THRESHOLD_THREE) { // Passing threshold 3 ends the pinch and snaps to the new state. if (startState == OVERVIEW && goingTowards == NORMAL) { mLauncher.getUserEventDispatcher().logActionOnContainer( Action.Touch.PINCH, Action.Direction.NONE, ContainerType.OVERVIEW, mWorkspace.getCurrentPage()); mLauncher.showWorkspace(true); mWorkspace.snapToPage(mWorkspace.getCurrentPage()); } else if (startState == NORMAL && goingTowards == OVERVIEW) { mLauncher.getUserEventDispatcher().logActionOnContainer( Action.Touch.PINCH, Action.Direction.NONE, ContainerType.WORKSPACE, mWorkspace.getCurrentPage()); mLauncher.showOverviewMode(true); } } else { Log.e(TAG, "Received unknown threshold to animate: " + threshold); } } private void setOverviewPanelsAlpha(float alpha, int duration) { int childCount = mWorkspace.getChildCount(); for (int i = 0; i < childCount; i++) { final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); if (duration == 0) { cl.setBackgroundAlpha(alpha); } else { ObjectAnimator.ofFloat(cl, "backgroundAlpha", alpha).setDuration(duration).start(); } } } private void animateHotseatAndQsb(boolean show) { startAnimator(INDEX_HOTSEAT, mWorkspace.createHotseatAlphaAnimator(show ? 1 : 0), THRESHOLD_ANIM_DURATION); } private void animateOverviewPanelButtons(boolean show) { animateShowHideView(INDEX_OVERVIEW_PANEL_BUTTONS, mLauncher.getOverviewPanel(), show); } private void animateScrim(boolean show) { float endValue = show ? mWorkspace.getStateTransitionAnimation().mWorkspaceScrimAlpha : 0; startAnimator(INDEX_SCRIM, ObjectAnimator.ofFloat(mLauncher.getDragLayer(), "backgroundAlpha", endValue), mNormalOverviewTransitionDuration); } private void animateShowHideView(int index, final View view, boolean show) { Animator animator = ObjectAnimator.ofFloat(view, View.ALPHA, show ? 1 : 0); animator.addListener(new AnimationLayerSet(view)); if (show) { view.setVisibility(View.VISIBLE); } else { animator.addListener(new AnimatorListenerAdapter() { private boolean mCancelled = false; @Override public void onAnimationCancel(Animator animation) { mCancelled = true; } @Override public void onAnimationEnd(Animator animation) { if (!mCancelled) { view.setVisibility(View.INVISIBLE); } } }); } startAnimator(index, animator, THRESHOLD_ANIM_DURATION); } private void startAnimator(int index, Animator animator, long duration) { if (mAnimators[index] != null) { mAnimators[index].cancel(); } mAnimators[index] = animator; mAnimators[index].setInterpolator(INTERPOLATOR); mAnimators[index].setDuration(duration).start(); } }