/* * 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 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_QSB = 1; private static final int INDEX_OVERVIEW_PANEL_BUTTONS = 2; private static final int INDEX_SCRIM = 3; private final Animator[] mAnimators = new Animator[4]; private final int[] mVisiblePageRange = new int[2]; 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.onLauncherTransitionEnd(mLauncher, false, true); } }); 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.showWorkspace(true); mWorkspace.snapToPage(mWorkspace.getCurrentPage()); } else if (startState == NORMAL && goingTowards == OVERVIEW) { mLauncher.showOverviewMode(true); } } else { Log.e(TAG, "Received unknown threshold to animate: " + threshold); } } private void setOverviewPanelsAlpha(float alpha, int duration) { mWorkspace.getVisiblePages(mVisiblePageRange); for (int i = mVisiblePageRange[0]; i <= mVisiblePageRange[1]; i++) { View page = mWorkspace.getPageAt(i); if (!mWorkspace.shouldDrawChild(page)) { continue; } if (duration == 0) { ((CellLayout) page).setBackgroundAlpha(alpha); } else { ObjectAnimator.ofFloat(page, "backgroundAlpha", alpha) .setDuration(duration).start(); } } } private void animateHotseatAndQsb(boolean show) { startAnimator(INDEX_HOTSEAT, mWorkspace.createHotseatAlphaAnimator(show ? 1 : 0), THRESHOLD_ANIM_DURATION); startAnimator(INDEX_QSB, mWorkspace.mQsbAlphaController.animateAlphaAtIndex( show ? 1 : 0, Workspace.QSB_ALPHA_INDEX_STATE_CHANGE), 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 = new LauncherViewPropertyAnimator(view).alpha(show ? 1 : 0).withLayer(); if (show) { view.setVisibility(View.VISIBLE); } else { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { 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].removeAllListeners(); mAnimators[index].cancel(); } mAnimators[index] = animator; mAnimators[index].setInterpolator(INTERPOLATOR); mAnimators[index].setDuration(duration).start(); } }