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