1 /* 2 * Copyright (C) 2015 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.launcher3.allapps; 17 18 import static com.android.app.animation.Interpolators.DECELERATE_1_7; 19 import static com.android.app.animation.Interpolators.INSTANT; 20 import static com.android.app.animation.Interpolators.LINEAR; 21 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; 22 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; 23 import static com.android.launcher3.LauncherState.ALL_APPS; 24 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT; 25 import static com.android.launcher3.LauncherState.BACKGROUND_APP; 26 import static com.android.launcher3.LauncherState.NORMAL; 27 import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER; 28 import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree; 29 import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree; 30 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER; 31 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_BOTTOM_SHEET_FADE; 32 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE; 33 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS; 34 import static com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV; 35 import static com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV; 36 import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS; 37 38 import android.animation.Animator; 39 import android.animation.ObjectAnimator; 40 import android.animation.ValueAnimator; 41 import android.util.FloatProperty; 42 import android.view.HapticFeedbackConstants; 43 import android.view.View; 44 import android.view.animation.Interpolator; 45 46 import androidx.annotation.FloatRange; 47 import androidx.annotation.Nullable; 48 49 import com.android.app.animation.Interpolators; 50 import com.android.launcher3.DeviceProfile; 51 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; 52 import com.android.launcher3.Launcher; 53 import com.android.launcher3.LauncherState; 54 import com.android.launcher3.R; 55 import com.android.launcher3.Utilities; 56 import com.android.launcher3.anim.AnimatedFloat; 57 import com.android.launcher3.anim.PendingAnimation; 58 import com.android.launcher3.anim.PropertySetter; 59 import com.android.launcher3.config.FeatureFlags; 60 import com.android.launcher3.statemanager.StateManager.StateHandler; 61 import com.android.launcher3.states.StateAnimationConfig; 62 import com.android.launcher3.touch.AllAppsSwipeController; 63 import com.android.launcher3.util.MultiPropertyFactory; 64 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; 65 import com.android.launcher3.util.MultiValueAlpha; 66 import com.android.launcher3.util.ScrollableLayoutManager; 67 import com.android.launcher3.util.Themes; 68 import com.android.launcher3.util.VibratorWrapper; 69 import com.android.launcher3.views.ScrimView; 70 71 /** 72 * Handles AllApps view transition. 73 * 1) Slides all apps view using direct manipulation 74 * 2) When finger is released, animate to either top or bottom accordingly. 75 * <p/> 76 * Algorithm: 77 * If release velocity > THRES1, snap according to the direction of movement. 78 * If release velocity < THRES1, snap according to either top or bottom depending on whether it's 79 * closer to top or closer to the page indicator. 80 */ 81 public class AllAppsTransitionController 82 implements StateHandler<LauncherState>, OnDeviceProfileChangeListener { 83 // This constant should match the second derivative of the animator interpolator. 84 public static final float INTERP_COEFF = 1.7f; 85 public static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200; 86 87 private static final float NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.1f; 88 private static final float SWIPE_DRAG_COMMIT_THRESHOLD = 89 1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL; 90 91 public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS = 92 new FloatProperty<AllAppsTransitionController>("allAppsProgress") { 93 94 @Override 95 public Float get(AllAppsTransitionController controller) { 96 return controller.mProgress; 97 } 98 99 @Override 100 public void setValue(AllAppsTransitionController controller, float progress) { 101 controller.setProgress(progress); 102 } 103 }; 104 105 private static final float ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT = 0f; 106 107 public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PULL_BACK_TRANSLATION = 108 new FloatProperty<AllAppsTransitionController>("allAppsPullBackTranslation") { 109 110 @Override 111 public Float get(AllAppsTransitionController controller) { 112 if (controller.mIsTablet) { 113 return controller.mAppsView.getActiveRecyclerView().getTranslationY(); 114 } else { 115 return controller.getAppsViewPullbackTranslationY().getValue(); 116 } 117 } 118 119 @Override 120 public void setValue(AllAppsTransitionController controller, float translation) { 121 if (controller.mIsTablet) { 122 controller.mAppsView.getActiveRecyclerView().setTranslationY(translation); 123 controller.getAppsViewPullbackTranslationY().setValue( 124 ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT); 125 } else { 126 controller.getAppsViewPullbackTranslationY().setValue(translation); 127 controller.mAppsView.getActiveRecyclerView().setTranslationY( 128 ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT); 129 } 130 } 131 }; 132 133 private static final float ALL_APPS_PULL_BACK_ALPHA_DEFAULT = 1f; 134 135 public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PULL_BACK_ALPHA = 136 new FloatProperty<AllAppsTransitionController>("allAppsPullBackAlpha") { 137 138 @Override 139 public Float get(AllAppsTransitionController controller) { 140 if (controller.mIsTablet) { 141 return controller.mAppsView.getActiveRecyclerView().getAlpha(); 142 } else { 143 return controller.getAppsViewPullbackAlpha().getValue(); 144 } 145 } 146 147 @Override 148 public void setValue(AllAppsTransitionController controller, float alpha) { 149 if (controller.mIsTablet) { 150 controller.mAppsView.getActiveRecyclerView().setAlpha(alpha); 151 controller.getAppsViewPullbackAlpha().setValue( 152 ALL_APPS_PULL_BACK_ALPHA_DEFAULT); 153 } else { 154 controller.getAppsViewPullbackAlpha().setValue(alpha); 155 controller.mAppsView.getActiveRecyclerView().setAlpha( 156 ALL_APPS_PULL_BACK_ALPHA_DEFAULT); 157 } 158 } 159 }; 160 161 private static final int INDEX_APPS_VIEW_PROGRESS = 0; 162 private static final int INDEX_APPS_VIEW_PULLBACK = 1; 163 private static final int APPS_VIEW_INDEX_COUNT = 2; 164 165 private ActivityAllAppsContainerView<Launcher> mAppsView; 166 167 private final Launcher mLauncher; 168 private final AnimatedFloat mAllAppScale = new AnimatedFloat(this::onScaleProgressChanged); 169 private final int mNavScrimFlag; 170 171 @Nullable private Animator.AnimatorListener mAllAppsSearchBackAnimationListener; 172 173 private boolean mIsVerticalLayout; 174 175 // Animation in this class is controlled by a single variable {@link mProgress}. 176 // Visually, it represents top y coordinate of the all apps container if multiplied with 177 // {@link mShiftRange}. 178 179 // When {@link mProgress} is 0, all apps container is pulled up. 180 // When {@link mProgress} is 1, all apps container is pulled down. 181 private float mShiftRange; // changes depending on the orientation 182 private float mProgress; // [0, 1], mShiftRange * mProgress = shiftCurrent 183 184 private ScrimView mScrimView; 185 186 private MultiValueAlpha mAppsViewAlpha; 187 private MultiPropertyFactory<View> mAppsViewTranslationY; 188 189 private boolean mIsTablet; 190 191 private boolean mHasScaleEffect; 192 private final VibratorWrapper mVibratorWrapper; 193 AllAppsTransitionController(Launcher l)194 public AllAppsTransitionController(Launcher l) { 195 mLauncher = l; 196 DeviceProfile dp = mLauncher.getDeviceProfile(); 197 mProgress = 1f; 198 mIsVerticalLayout = dp.isVerticalBarLayout(); 199 mIsTablet = dp.isTablet; 200 mNavScrimFlag = Themes.getAttrBoolean(l, R.attr.isMainColorDark) 201 ? FLAG_DARK_NAV : FLAG_LIGHT_NAV; 202 203 setShiftRange(dp.allAppsShiftRange); 204 mAllAppScale.value = 1; 205 mLauncher.addOnDeviceProfileChangeListener(this); 206 mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext()); 207 } 208 getShiftRange()209 public float getShiftRange() { 210 return mShiftRange; 211 } 212 213 @Override onDeviceProfileChanged(DeviceProfile dp)214 public void onDeviceProfileChanged(DeviceProfile dp) { 215 mIsVerticalLayout = dp.isVerticalBarLayout(); 216 setShiftRange(dp.allAppsShiftRange); 217 218 if (mIsVerticalLayout) { 219 mLauncher.getHotseat().setTranslationY(0); 220 mLauncher.getWorkspace().getPageIndicator().setTranslationY(0); 221 } 222 223 mIsTablet = dp.isTablet; 224 } 225 226 /** 227 * Note this method should not be called outside this class. This is public because it is used 228 * in xml-based animations which also handle updating the appropriate UI. 229 * 230 * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace 231 * @see #setState(LauncherState) 232 * @see #setStateWithAnimation(LauncherState, StateAnimationConfig, PendingAnimation) 233 */ setProgress(float progress)234 public void setProgress(float progress) { 235 mProgress = progress; 236 boolean fromBackground = 237 mLauncher.getStateManager().getCurrentStableState() == BACKGROUND_APP; 238 // Allow apps panel to shift the full screen if coming from another app. 239 float shiftRange = fromBackground ? mLauncher.getDeviceProfile().heightPx : mShiftRange; 240 getAppsViewProgressTranslationY().setValue(mProgress * shiftRange); 241 mLauncher.onAllAppsTransition(1 - progress); 242 243 boolean hasScrim = progress < NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD 244 && mLauncher.getAppsView().getNavBarScrimHeight() > 0; 245 mLauncher.getSystemUiController().updateUiState( 246 UI_STATE_ALL_APPS, hasScrim ? mNavScrimFlag : 0); 247 } 248 getProgress()249 public float getProgress() { 250 return mProgress; 251 } 252 getAppsViewProgressTranslationY()253 private MultiProperty getAppsViewProgressTranslationY() { 254 return mAppsViewTranslationY.get(INDEX_APPS_VIEW_PROGRESS); 255 } 256 getAppsViewPullbackTranslationY()257 private MultiProperty getAppsViewPullbackTranslationY() { 258 return mAppsViewTranslationY.get(INDEX_APPS_VIEW_PULLBACK); 259 } 260 getAppsViewProgressAlpha()261 private MultiProperty getAppsViewProgressAlpha() { 262 return mAppsViewAlpha.get(INDEX_APPS_VIEW_PROGRESS); 263 } 264 getAppsViewPullbackAlpha()265 private MultiProperty getAppsViewPullbackAlpha() { 266 return mAppsViewAlpha.get(INDEX_APPS_VIEW_PULLBACK); 267 } 268 269 /** 270 * Sets the vertical transition progress to {@param state} and updates all the dependent UI 271 * accordingly. 272 */ 273 @Override setState(LauncherState state)274 public void setState(LauncherState state) { 275 setProgress(state.getVerticalProgress(mLauncher)); 276 setAlphas(state, new StateAnimationConfig(), NO_ANIM_PROPERTY_SETTER); 277 } 278 279 @Override onBackProgressed( LauncherState toState, @FloatRange(from = 0.0, to = 1.0) float backProgress)280 public void onBackProgressed( 281 LauncherState toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) { 282 if (!mLauncher.isInState(ALL_APPS) || !NORMAL.equals(toState)) { 283 return; 284 } 285 286 float deceleratedProgress = Interpolators.BACK_GESTURE.getInterpolation(backProgress); 287 float scaleProgress = ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE 288 + (1 - ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE) 289 * (1 - deceleratedProgress); 290 291 mAllAppScale.updateValue(scaleProgress); 292 } 293 onScaleProgressChanged()294 private void onScaleProgressChanged() { 295 final float scaleProgress = mAllAppScale.value; 296 SCALE_PROPERTY.set(mLauncher.getAppsView(), scaleProgress); 297 if (!mLauncher.getAppsView().isSearching() || !mLauncher.getDeviceProfile().isTablet) { 298 mLauncher.getScrimView().setScrimHeaderScale(scaleProgress); 299 } 300 301 AllAppsRecyclerView rv = mLauncher.getAppsView().getActiveRecyclerView(); 302 303 // Disable view clipping from all apps' RecyclerView up to all apps view during scale 304 // animation, and vice versa. The goal is to display extra roll(s) app icons (rendered in 305 // {@link AppsGridLayoutManager#calculateExtraLayoutSpace}) during scale animation. 306 boolean hasScaleEffect = scaleProgress < 1f; 307 if (hasScaleEffect != mHasScaleEffect) { 308 mHasScaleEffect = hasScaleEffect; 309 if (mHasScaleEffect) { 310 modifyAttributesOnViewTree(rv, mLauncher.getAppsView(), 311 CLIP_CHILDREN_FALSE_MODIFIER); 312 } else { 313 restoreAttributesOnViewTree(rv, mLauncher.getAppsView(), 314 CLIP_CHILDREN_FALSE_MODIFIER); 315 } 316 } 317 } 318 319 /** Set {@link Animator.AnimatorListener} for scaling all apps scale to 1 animation. */ 320 public void setAllAppsSearchBackAnimationListener(Animator.AnimatorListener listener) { 321 mAllAppsSearchBackAnimationListener = listener; 322 } 323 324 /** 325 * Animate all apps view to 1f scale. This is called when backing (exiting) from all apps 326 * search view to all apps view. 327 */ 328 public void animateAllAppsToNoScale() { 329 if (mAllAppScale.isAnimating()) { 330 return; 331 } 332 Animator animator = mAllAppScale.animateToValue(1f) 333 .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS); 334 if (mAllAppsSearchBackAnimationListener != null) { 335 animator.addListener(mAllAppsSearchBackAnimationListener); 336 } 337 animator.start(); 338 } 339 340 /** 341 * Creates an animation which updates the vertical transition progress and updates all the 342 * dependent UI using various animation events 343 * 344 * This method also dictates where along the progress the haptics should be played. As the user 345 * scrolls up from workspace or down from AllApps, a drag haptic is being played until the 346 * commit point where it plays a commit haptic. Where we play the haptics differs when going 347 * from workspace -> allApps and vice versa. 348 */ 349 @Override 350 public void setStateWithAnimation(LauncherState toState, 351 StateAnimationConfig config, PendingAnimation builder) { 352 if (mLauncher.isInState(ALL_APPS) && !ALL_APPS.equals(toState)) { 353 builder.addEndListener(success -> { 354 // Reset pull back progress and alpha after switching states. 355 ALL_APPS_PULL_BACK_TRANSLATION.set(this, ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT); 356 ALL_APPS_PULL_BACK_ALPHA.set(this, ALL_APPS_PULL_BACK_ALPHA_DEFAULT); 357 358 mAllAppScale.updateValue(1f); 359 }); 360 } 361 362 if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && config.isUserControlled() 363 && Utilities.ATLEAST_S) { 364 if (toState == ALL_APPS) { builder.addOnFrameListener( new VibrationAnimatorUpdateListener(this, mVibratorWrapper, SWIPE_DRAG_COMMIT_THRESHOLD, 1))365 builder.addOnFrameListener( 366 new VibrationAnimatorUpdateListener(this, mVibratorWrapper, 367 SWIPE_DRAG_COMMIT_THRESHOLD, 1)); 368 } else { builder.addOnFrameListener( new VibrationAnimatorUpdateListener(this, mVibratorWrapper, 0, SWIPE_DRAG_COMMIT_THRESHOLD))369 builder.addOnFrameListener( 370 new VibrationAnimatorUpdateListener(this, mVibratorWrapper, 371 0, SWIPE_DRAG_COMMIT_THRESHOLD)); 372 } builder.addEndListener(unused)373 builder.addEndListener((unused) -> { 374 mVibratorWrapper.cancelVibrate(); 375 }); 376 } 377 378 float targetProgress = toState.getVerticalProgress(mLauncher); 379 if (Float.compare(mProgress, targetProgress) == 0) { setAlphas(toState, config, builder)380 setAlphas(toState, config, builder); 381 // Fail fast 382 return; 383 } 384 385 // need to decide depending on the release velocity 386 Interpolator verticalProgressInterpolator = config.getInterpolator(ANIM_VERTICAL_PROGRESS, 387 config.isUserControlled() ? LINEAR : DECELERATE_1_7); 388 Animator anim = createSpringAnimation(mProgress, targetProgress); anim.setInterpolator(verticalProgressInterpolator)389 anim.setInterpolator(verticalProgressInterpolator); builder.add(anim)390 builder.add(anim); 391 setAlphas(toState, config, builder)392 setAlphas(toState, config, builder); 393 // This controls both haptics for tapping on QSB and going to all apps. 394 if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL) && 395 !FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get()) { 396 mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 397 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 398 } 399 } 400 createSpringAnimation(float... progressValues)401 public Animator createSpringAnimation(float... progressValues) { 402 return ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, progressValues); 403 } 404 405 /** 406 * Updates the property for the provided state 407 */ setAlphas(LauncherState state, StateAnimationConfig config, PropertySetter setter)408 public void setAlphas(LauncherState state, StateAnimationConfig config, PropertySetter setter) { 409 int visibleElements = state.getVisibleElements(mLauncher); 410 boolean hasAllAppsContent = (visibleElements & ALL_APPS_CONTENT) != 0; 411 412 Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR); 413 setter.setFloat(getAppsViewProgressAlpha(), MultiPropertyFactory.MULTI_PROPERTY_VALUE, 414 hasAllAppsContent ? 1 : 0, allAppsFade); 415 setter.setFloat(getAppsViewPullbackAlpha(), MultiPropertyFactory.MULTI_PROPERTY_VALUE, 416 hasAllAppsContent ? 1 : 0, allAppsFade); 417 418 setter.setFloat(mLauncher.getAppsView(), 419 ActivityAllAppsContainerView.BOTTOM_SHEET_ALPHA, hasAllAppsContent ? 1 : 0, 420 config.getInterpolator(ANIM_ALL_APPS_BOTTOM_SHEET_FADE, INSTANT)); 421 422 boolean shouldProtectHeader = !config.hasAnimationFlag(StateAnimationConfig.SKIP_SCRIM) 423 && (ALL_APPS == state || mLauncher.getStateManager().getState() == ALL_APPS); 424 mScrimView.setDrawingController(shouldProtectHeader ? mAppsView : null); 425 } 426 427 /** 428 * see Launcher#setupViews 429 */ setupViews(ScrimView scrimView, ActivityAllAppsContainerView<Launcher> appsView)430 public void setupViews(ScrimView scrimView, ActivityAllAppsContainerView<Launcher> appsView) { 431 mScrimView = scrimView; 432 mAppsView = appsView; 433 mAppsView.setScrimView(scrimView); 434 435 mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT, 436 FeatureFlags.ALL_APPS_GONE_VISIBILITY.get() ? View.GONE : View.INVISIBLE); 437 mAppsViewAlpha.setUpdateVisibility(true); 438 mAppsViewTranslationY = new MultiPropertyFactory<>( 439 mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum); 440 } 441 442 /** 443 * Updates the total scroll range but does not update the UI. 444 */ setShiftRange(float shiftRange)445 public void setShiftRange(float shiftRange) { 446 mShiftRange = shiftRange; 447 } 448 449 /** 450 * This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start 451 * threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller 452 * as it gives an accurate progress that dictates where the vibrator should vibrate. 453 * Note: once the user begins a gesture and does the commit haptic, there should not be anymore 454 * haptics played for that gesture. 455 */ 456 private static class VibrationAnimatorUpdateListener implements 457 ValueAnimator.AnimatorUpdateListener { 458 private final VibratorWrapper mVibratorWrapper; 459 private final AllAppsTransitionController mController; 460 private final float mStartThreshold; 461 private final float mEndThreshold; 462 private boolean mHasCommitted; 463 VibrationAnimatorUpdateListener(AllAppsTransitionController controller, VibratorWrapper vibratorWrapper, float startThreshold, float endThreshold)464 VibrationAnimatorUpdateListener(AllAppsTransitionController controller, 465 VibratorWrapper vibratorWrapper, float startThreshold, 466 float endThreshold) { 467 mController = controller; 468 mVibratorWrapper = vibratorWrapper; 469 mStartThreshold = startThreshold; 470 mEndThreshold = endThreshold; 471 } 472 473 @Override onAnimationUpdate(ValueAnimator animation)474 public void onAnimationUpdate(ValueAnimator animation) { 475 if (mHasCommitted) { 476 return; 477 } 478 float currentProgress = 479 AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController); 480 if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) { 481 mVibratorWrapper.vibrateForDragTexture(); 482 } else if (!(currentProgress == 0 || currentProgress == 1)) { 483 // This check guards against committing at the location of the start of the gesture 484 mVibratorWrapper.vibrateForDragCommit(); 485 mHasCommitted = true; 486 } 487 } 488 } 489 } 490