1 /* 2 * Copyright (C) 2020 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 package com.android.quickstep.util; 17 18 import static com.android.app.animation.Interpolators.DECELERATE; 19 import static com.android.app.animation.Interpolators.LINEAR; 20 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; 21 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; 22 23 import android.animation.TimeInterpolator; 24 import android.content.Context; 25 import android.graphics.Matrix; 26 import android.graphics.PointF; 27 import android.graphics.Rect; 28 import android.graphics.RectF; 29 import android.util.FloatProperty; 30 31 import androidx.annotation.Nullable; 32 33 import com.android.launcher3.DeviceProfile; 34 import com.android.launcher3.Launcher; 35 import com.android.launcher3.Utilities; 36 import com.android.launcher3.anim.AnimatorPlaybackController; 37 import com.android.launcher3.anim.PendingAnimation; 38 import com.android.quickstep.orientation.RecentsPagedOrientationHandler; 39 import com.android.quickstep.views.RecentsView; 40 41 /** 42 * Controls an animation that can go beyond progress = 1, at which point resistance should be 43 * applied. Internally, this is a wrapper around 2 {@link AnimatorPlaybackController}s, one that 44 * runs from progress 0 to 1 like normal, then one that seamlessly continues that animation but 45 * starts applying resistance as well. 46 */ 47 public class AnimatorControllerWithResistance { 48 49 private enum RecentsResistanceParams { 50 FROM_APP(0.75f, 0.5f, 1f, false), 51 FROM_APP_TABLET(1f, 0.7f, 1f, true), 52 FROM_OVERVIEW(1f, 0.75f, 0.5f, false); 53 RecentsResistanceParams(float scaleStartResist, float scaleMaxResist, float translationFactor, boolean stopScalingAtTop)54 RecentsResistanceParams(float scaleStartResist, float scaleMaxResist, 55 float translationFactor, boolean stopScalingAtTop) { 56 this.scaleStartResist = scaleStartResist; 57 this.scaleMaxResist = scaleMaxResist; 58 this.translationFactor = translationFactor; 59 this.stopScalingAtTop = stopScalingAtTop; 60 } 61 62 /** 63 * Start slowing down the rate of scaling down when recents view is smaller than this scale. 64 */ 65 public final float scaleStartResist; 66 67 /** 68 * Recents view will reach this scale at the very end of the drag. 69 */ 70 public final float scaleMaxResist; 71 72 /** 73 * How much translation to apply to RecentsView when the drag reaches the top of the screen, 74 * where 0 will keep it centered and 1 will have it barely touch the top of the screen. 75 */ 76 public final float translationFactor; 77 78 /** 79 * Whether to end scaling effect when the scaled down version of TaskView's top reaches the 80 * non-scaled version of TaskView's top. 81 */ 82 public final boolean stopScalingAtTop; 83 } 84 85 private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DECELERATE; 86 private static final TimeInterpolator RECENTS_TRANSLATE_RESIST_INTERPOLATOR = LINEAR; 87 88 private static final Rect TEMP_RECT = new Rect(); 89 90 private final AnimatorPlaybackController mNormalController; 91 private final AnimatorPlaybackController mResistanceController; 92 93 // Initialize to -1 so the first 0 gets applied. 94 private float mLastNormalProgress = -1; 95 private float mLastResistProgress; 96 AnimatorControllerWithResistance(AnimatorPlaybackController normalController, AnimatorPlaybackController resistanceController)97 public AnimatorControllerWithResistance(AnimatorPlaybackController normalController, 98 AnimatorPlaybackController resistanceController) { 99 mNormalController = normalController; 100 mResistanceController = resistanceController; 101 } 102 getNormalController()103 public AnimatorPlaybackController getNormalController() { 104 return mNormalController; 105 } 106 107 /** 108 * Applies the current progress of the animation. 109 * @param progress From 0 to maxProgress, where 1 is the target we are animating towards. 110 * @param maxProgress > 1, this is where the resistance will be applied. 111 */ setProgress(float progress, float maxProgress)112 public void setProgress(float progress, float maxProgress) { 113 float normalProgress = Utilities.boundToRange(progress, 0, 1); 114 if (normalProgress != mLastNormalProgress) { 115 mLastNormalProgress = normalProgress; 116 mNormalController.setPlayFraction(normalProgress); 117 } 118 if (maxProgress <= 1) { 119 return; 120 } 121 float resistProgress = progress <= 1 ? 0 : Utilities.getProgress(progress, 1, maxProgress); 122 if (resistProgress != mLastResistProgress) { 123 mLastResistProgress = resistProgress; 124 mResistanceController.setPlayFraction(resistProgress); 125 } 126 } 127 128 /** 129 * Applies resistance to recents when swiping up past its target position. 130 * @param normalController The controller to run from 0 to 1 before this resistance applies. 131 * @param context Used to compute start and end values. 132 * @param recentsOrientedState Used to compute start and end values. 133 * @param dp Used to compute start and end values. 134 * @param scaleTarget The target for the scaleProperty. 135 * @param scaleProperty Animate the value to change the scale of the window/recents view. 136 * @param translationTarget The target for the translationProperty. 137 * @param translationProperty Animate the value to change the translation of the recents view. 138 */ createForRecents( AnimatorPlaybackController normalController, Context context, RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget, FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget, FloatProperty<TRANSLATION> translationProperty)139 public static <SCALE, TRANSLATION> AnimatorControllerWithResistance createForRecents( 140 AnimatorPlaybackController normalController, Context context, 141 RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget, 142 FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget, 143 FloatProperty<TRANSLATION> translationProperty) { 144 145 RecentsParams params = new RecentsParams(context, recentsOrientedState, dp, scaleTarget, 146 scaleProperty, translationTarget, translationProperty); 147 PendingAnimation resistAnim = createRecentsResistanceAnim(params); 148 AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController(); 149 return new AnimatorControllerWithResistance(normalController, resistanceController); 150 } 151 152 /** 153 * Creates the resistance animation for {@link #createForRecents}, or can be used separately 154 * when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}. 155 */ createRecentsResistanceAnim( RecentsParams<SCALE, TRANSLATION> params)156 public static <SCALE, TRANSLATION> PendingAnimation createRecentsResistanceAnim( 157 RecentsParams<SCALE, TRANSLATION> params) { 158 Rect startRect = new Rect(); 159 RecentsPagedOrientationHandler orientationHandler = params.recentsOrientedState 160 .getOrientationHandler(); 161 params.recentsOrientedState.getContainerInterface() 162 .calculateTaskSize(params.context, params.dp, startRect, orientationHandler); 163 long distanceToCover = startRect.bottom; 164 PendingAnimation resistAnim = params.resistAnim != null 165 ? params.resistAnim 166 : new PendingAnimation(distanceToCover * 2); 167 168 PointF pivot = new PointF(); 169 float fullscreenScale = params.recentsOrientedState.getFullScreenScaleAndPivot( 170 startRect, params.dp, pivot); 171 172 // Compute where the task view would be based on the end scale. 173 RectF endRectF = new RectF(startRect); 174 Matrix temp = new Matrix(); 175 temp.setScale(params.resistanceParams.scaleMaxResist, 176 params.resistanceParams.scaleMaxResist, pivot.x, pivot.y); 177 temp.mapRect(endRectF); 178 // Translate such that the task view touches the top of the screen when drag does. 179 float endTranslation = endRectF.top 180 * orientationHandler.getSecondaryTranslationDirectionFactor() 181 * params.resistanceParams.translationFactor; 182 resistAnim.addFloat(params.translationTarget, params.translationProperty, 183 params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR); 184 185 float prevScaleRate = (fullscreenScale - params.startScale) 186 / (params.dp.heightPx - startRect.bottom); 187 // This is what the scale would be at the end of the drag if we didn't apply resistance. 188 float endScale = params.startScale - prevScaleRate * distanceToCover; 189 // Create an interpolator that resists the scale so the scale doesn't get smaller than 190 // RECENTS_SCALE_MAX_RESIST. 191 float startResist = Utilities.getProgress(params.resistanceParams.scaleStartResist, 192 params.startScale, endScale); 193 float maxResist = Utilities.getProgress(params.resistanceParams.scaleMaxResist, 194 params.startScale, endScale); 195 float stopResist = 196 params.resistanceParams.stopScalingAtTop ? 1f - startRect.top / endRectF.top : 1f; 197 final TimeInterpolator scaleInterpolator = t -> { 198 if (t <= startResist) { 199 return t; 200 } 201 if (t >= stopResist) { 202 return maxResist; 203 } 204 float resistProgress = Utilities.getProgress(t, startResist, stopResist); 205 resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress); 206 return startResist + resistProgress * (maxResist - startResist); 207 }; 208 resistAnim.addFloat(params.scaleTarget, params.scaleProperty, params.startScale, endScale, 209 scaleInterpolator); 210 211 return resistAnim; 212 } 213 214 /** 215 * Helper method to update or create a PendingAnimation suitable for animating 216 * a RecentsView interaction that started from the overview state. 217 */ createRecentsResistanceFromOverviewAnim( Launcher launcher, @Nullable PendingAnimation resistanceAnim)218 public static PendingAnimation createRecentsResistanceFromOverviewAnim( 219 Launcher launcher, @Nullable PendingAnimation resistanceAnim) { 220 RecentsView recentsView = launcher.getOverviewPanel(); 221 RecentsParams params = new RecentsParams(launcher, recentsView.getPagedViewOrientedState(), 222 launcher.getDeviceProfile(), recentsView, RECENTS_SCALE_PROPERTY, recentsView, 223 TASK_SECONDARY_TRANSLATION) 224 .setResistAnim(resistanceAnim) 225 .setResistanceParams(RecentsResistanceParams.FROM_OVERVIEW) 226 .setStartScale(recentsView.getScaleX()); 227 return createRecentsResistanceAnim(params); 228 } 229 230 /** 231 * Params to compute resistance when scaling/translating recents. 232 */ 233 private static class RecentsParams<SCALE, TRANSLATION> { 234 // These are all required and can't have default values, hence are final. 235 public final Context context; 236 public final RecentsOrientedState recentsOrientedState; 237 public final DeviceProfile dp; 238 public final SCALE scaleTarget; 239 public final FloatProperty<SCALE> scaleProperty; 240 public final TRANSLATION translationTarget; 241 public final FloatProperty<TRANSLATION> translationProperty; 242 243 // These are not required, or can have a default value that is generally correct. 244 @Nullable public PendingAnimation resistAnim = null; 245 public RecentsResistanceParams resistanceParams; 246 public float startScale = 1f; 247 public float startTranslation = 0f; 248 RecentsParams(Context context, RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget, FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget, FloatProperty<TRANSLATION> translationProperty)249 private RecentsParams(Context context, RecentsOrientedState recentsOrientedState, 250 DeviceProfile dp, SCALE scaleTarget, FloatProperty<SCALE> scaleProperty, 251 TRANSLATION translationTarget, FloatProperty<TRANSLATION> translationProperty) { 252 this.context = context; 253 this.recentsOrientedState = recentsOrientedState; 254 this.dp = dp; 255 this.scaleTarget = scaleTarget; 256 this.scaleProperty = scaleProperty; 257 this.translationTarget = translationTarget; 258 this.translationProperty = translationProperty; 259 if (dp.isTablet) { 260 resistanceParams = RecentsResistanceParams.FROM_APP_TABLET; 261 } else { 262 resistanceParams = RecentsResistanceParams.FROM_APP; 263 } 264 } 265 setResistAnim(PendingAnimation resistAnim)266 private RecentsParams setResistAnim(PendingAnimation resistAnim) { 267 this.resistAnim = resistAnim; 268 return this; 269 } 270 setResistanceParams(RecentsResistanceParams resistanceParams)271 private RecentsParams setResistanceParams(RecentsResistanceParams resistanceParams) { 272 this.resistanceParams = resistanceParams; 273 return this; 274 } 275 setStartScale(float startScale)276 private RecentsParams setStartScale(float startScale) { 277 this.startScale = startScale; 278 return this; 279 } 280 setStartTranslation(float startTranslation)281 private RecentsParams setStartTranslation(float startTranslation) { 282 this.startTranslation = startTranslation; 283 return this; 284 } 285 } 286 } 287