1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.util.Log; 24 import android.view.View; 25 import android.view.animation.LinearInterpolator; 26 27 import com.android.launcher3.anim.AnimationLayerSet; 28 import com.android.launcher3.userevent.nano.LauncherLogProto.Action; 29 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 30 31 import static com.android.launcher3.Workspace.State.NORMAL; 32 import static com.android.launcher3.Workspace.State.OVERVIEW; 33 34 /** 35 * Manages the animations that play as the user pinches to/from overview mode. 36 * 37 * It will look like this pinching in: 38 * - Workspace scales down 39 * - At some threshold 1, hotseat and QSB fade out (full animation) 40 * - At a later threshold 2, panel buttons fade in and scrim fades in 41 * - At a final threshold 3, snap to overview 42 * 43 * Pinching out: 44 * - Workspace scales up 45 * - At threshold 1, panel buttons fade out 46 * - At threshold 2, hotseat and QSB fade in and scrim fades out 47 * - At threshold 3, snap to workspace 48 * 49 * @see PinchToOverviewListener 50 * @see PinchThresholdManager 51 */ 52 public class PinchAnimationManager { 53 private static final String TAG = "PinchAnimationManager"; 54 55 private static final int THRESHOLD_ANIM_DURATION = 150; 56 private static final LinearInterpolator INTERPOLATOR = new LinearInterpolator(); 57 58 private static final int INDEX_HOTSEAT = 0; 59 private static final int INDEX_QSB = 1; 60 private static final int INDEX_OVERVIEW_PANEL_BUTTONS = 2; 61 private static final int INDEX_SCRIM = 3; 62 63 private final Animator[] mAnimators = new Animator[4]; 64 65 private Launcher mLauncher; 66 private Workspace mWorkspace; 67 68 private float mOverviewScale; 69 private float mOverviewTranslationY; 70 private int mNormalOverviewTransitionDuration; 71 private boolean mIsAnimating; 72 PinchAnimationManager(Launcher launcher)73 public PinchAnimationManager(Launcher launcher) { 74 mLauncher = launcher; 75 mWorkspace = launcher.mWorkspace; 76 77 mOverviewScale = mWorkspace.getOverviewModeShrinkFactor(); 78 mOverviewTranslationY = mWorkspace.getOverviewModeTranslationY(); 79 mNormalOverviewTransitionDuration = mWorkspace.getStateTransitionAnimation() 80 .mOverviewTransitionTime; 81 } 82 getNormalOverviewTransitionDuration()83 public int getNormalOverviewTransitionDuration() { 84 return mNormalOverviewTransitionDuration; 85 } 86 87 /** 88 * Interpolate from {@param currentProgress} to {@param toProgress}, calling 89 * {@link #setAnimationProgress(float)} throughout the duration. If duration is -1, 90 * the default overview transition duration is used. 91 */ animateToProgress(float currentProgress, float toProgress, int duration, final PinchThresholdManager thresholdManager)92 public void animateToProgress(float currentProgress, float toProgress, int duration, 93 final PinchThresholdManager thresholdManager) { 94 if (duration == -1) { 95 duration = mNormalOverviewTransitionDuration; 96 } 97 ValueAnimator animator = ValueAnimator.ofFloat(currentProgress, toProgress); 98 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 99 @Override 100 public void onAnimationUpdate(ValueAnimator animation) { 101 float pinchProgress = (Float) animation.getAnimatedValue(); 102 setAnimationProgress(pinchProgress); 103 thresholdManager.updateAndAnimatePassedThreshold(pinchProgress, 104 PinchAnimationManager.this); 105 } 106 } 107 ); 108 animator.addListener(new AnimatorListenerAdapter() { 109 @Override 110 public void onAnimationEnd(Animator animation) { 111 mIsAnimating = false; 112 thresholdManager.reset(); 113 mWorkspace.onEndStateTransition(); 114 } 115 }); 116 animator.setDuration(duration).start(); 117 mIsAnimating = true; 118 } 119 isAnimating()120 public boolean isAnimating() { 121 return mIsAnimating; 122 } 123 124 /** 125 * Animates to the specified progress. This should be called repeatedly throughout the pinch 126 * gesture to run animations that interpolate throughout the gesture. 127 * @param interpolatedProgress The progress from 0 to 1, where 0 is overview and 1 is workspace. 128 */ setAnimationProgress(float interpolatedProgress)129 public void setAnimationProgress(float interpolatedProgress) { 130 float interpolatedScale = interpolatedProgress * (1f - mOverviewScale) + mOverviewScale; 131 float interpolatedTranslationY = (1f - interpolatedProgress) * mOverviewTranslationY; 132 mWorkspace.setScaleX(interpolatedScale); 133 mWorkspace.setScaleY(interpolatedScale); 134 mWorkspace.setTranslationY(interpolatedTranslationY); 135 setOverviewPanelsAlpha(1f - interpolatedProgress, 0); 136 } 137 138 /** 139 * Animates certain properties based on which threshold was passed, and in what direction. The 140 * starting state must also be taken into account because the thresholds mean different things 141 * when going from workspace to overview and vice versa. 142 * @param threshold One of {@link PinchThresholdManager#THRESHOLD_ONE}, 143 * {@link PinchThresholdManager#THRESHOLD_TWO}, or 144 * {@link PinchThresholdManager#THRESHOLD_THREE} 145 * @param startState {@link Workspace.State#NORMAL} or {@link Workspace.State#OVERVIEW}. 146 * @param goingTowards {@link Workspace.State#NORMAL} or {@link Workspace.State#OVERVIEW}. 147 * Note that this doesn't have to be the opposite of startState; 148 */ animateThreshold(float threshold, Workspace.State startState, Workspace.State goingTowards)149 public void animateThreshold(float threshold, Workspace.State startState, 150 Workspace.State goingTowards) { 151 if (threshold == PinchThresholdManager.THRESHOLD_ONE) { 152 if (startState == OVERVIEW) { 153 animateOverviewPanelButtons(goingTowards == OVERVIEW); 154 } else if (startState == NORMAL) { 155 animateHotseatAndQsb(goingTowards == NORMAL); 156 } 157 } else if (threshold == PinchThresholdManager.THRESHOLD_TWO) { 158 if (startState == OVERVIEW) { 159 animateHotseatAndQsb(goingTowards == NORMAL); 160 animateScrim(goingTowards == OVERVIEW); 161 } else if (startState == NORMAL) { 162 animateOverviewPanelButtons(goingTowards == OVERVIEW); 163 animateScrim(goingTowards == OVERVIEW); 164 } 165 } else if (threshold == PinchThresholdManager.THRESHOLD_THREE) { 166 // Passing threshold 3 ends the pinch and snaps to the new state. 167 if (startState == OVERVIEW && goingTowards == NORMAL) { 168 mLauncher.getUserEventDispatcher().logActionOnContainer( 169 Action.Touch.PINCH, Action.Direction.NONE, 170 ContainerType.OVERVIEW, mWorkspace.getCurrentPage()); 171 mLauncher.showWorkspace(true); 172 mWorkspace.snapToPage(mWorkspace.getCurrentPage()); 173 } else if (startState == NORMAL && goingTowards == OVERVIEW) { 174 mLauncher.getUserEventDispatcher().logActionOnContainer( 175 Action.Touch.PINCH, Action.Direction.NONE, 176 ContainerType.WORKSPACE, mWorkspace.getCurrentPage()); 177 mLauncher.showOverviewMode(true); 178 } 179 } else { 180 Log.e(TAG, "Received unknown threshold to animate: " + threshold); 181 } 182 } 183 setOverviewPanelsAlpha(float alpha, int duration)184 private void setOverviewPanelsAlpha(float alpha, int duration) { 185 int childCount = mWorkspace.getChildCount(); 186 for (int i = 0; i < childCount; i++) { 187 final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); 188 if (duration == 0) { 189 cl.setBackgroundAlpha(alpha); 190 } else { 191 ObjectAnimator.ofFloat(cl, "backgroundAlpha", alpha).setDuration(duration).start(); 192 } 193 } 194 } 195 animateHotseatAndQsb(boolean show)196 private void animateHotseatAndQsb(boolean show) { 197 startAnimator(INDEX_HOTSEAT, 198 mWorkspace.createHotseatAlphaAnimator(show ? 1 : 0), THRESHOLD_ANIM_DURATION); 199 startAnimator(INDEX_QSB, mWorkspace.mQsbAlphaController.animateAlphaAtIndex( 200 show ? 1 : 0, Workspace.QSB_ALPHA_INDEX_STATE_CHANGE), THRESHOLD_ANIM_DURATION); 201 } 202 animateOverviewPanelButtons(boolean show)203 private void animateOverviewPanelButtons(boolean show) { 204 animateShowHideView(INDEX_OVERVIEW_PANEL_BUTTONS, mLauncher.getOverviewPanel(), show); 205 } 206 animateScrim(boolean show)207 private void animateScrim(boolean show) { 208 float endValue = show ? mWorkspace.getStateTransitionAnimation().mWorkspaceScrimAlpha : 0; 209 startAnimator(INDEX_SCRIM, 210 ObjectAnimator.ofFloat(mLauncher.getDragLayer(), "backgroundAlpha", endValue), 211 mNormalOverviewTransitionDuration); 212 } 213 animateShowHideView(int index, final View view, boolean show)214 private void animateShowHideView(int index, final View view, boolean show) { 215 Animator animator = ObjectAnimator.ofFloat(view, View.ALPHA, show ? 1 : 0); 216 animator.addListener(new AnimationLayerSet(view)); 217 if (show) { 218 view.setVisibility(View.VISIBLE); 219 } else { 220 animator.addListener(new AnimatorListenerAdapter() { 221 private boolean mCancelled = false; 222 223 @Override 224 public void onAnimationCancel(Animator animation) { 225 mCancelled = true; 226 } 227 228 @Override 229 public void onAnimationEnd(Animator animation) { 230 if (!mCancelled) { 231 view.setVisibility(View.INVISIBLE); 232 } 233 } 234 }); 235 } 236 startAnimator(index, animator, THRESHOLD_ANIM_DURATION); 237 } 238 startAnimator(int index, Animator animator, long duration)239 private void startAnimator(int index, Animator animator, long duration) { 240 if (mAnimators[index] != null) { 241 mAnimators[index].cancel(); 242 } 243 mAnimators[index] = animator; 244 mAnimators[index].setInterpolator(INTERPOLATOR); 245 mAnimators[index].setDuration(duration).start(); 246 } 247 } 248