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