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 17 package com.android.launcher3; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.TimeInterpolator; 24 import android.animation.ValueAnimator; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.accessibility.AccessibilityManager; 30 import android.view.accessibility.AccessibilityNodeInfo; 31 import android.view.animation.DecelerateInterpolator; 32 33 import com.android.launcher3.anim.AnimationLayerSet; 34 import com.android.launcher3.anim.PropertyListBuilder; 35 import com.android.launcher3.config.FeatureFlags; 36 import com.android.launcher3.dragndrop.DragLayer; 37 import com.android.launcher3.util.Thunk; 38 39 /** 40 * A convenience class to update a view's visibility state after an alpha animation. 41 */ 42 class AlphaUpdateListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener { 43 private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f; 44 45 private View mView; 46 private boolean mAccessibilityEnabled; 47 private boolean mCanceled = false; 48 AlphaUpdateListener(View v, boolean accessibilityEnabled)49 public AlphaUpdateListener(View v, boolean accessibilityEnabled) { 50 mView = v; 51 mAccessibilityEnabled = accessibilityEnabled; 52 } 53 54 @Override onAnimationUpdate(ValueAnimator arg0)55 public void onAnimationUpdate(ValueAnimator arg0) { 56 updateVisibility(mView, mAccessibilityEnabled); 57 } 58 updateVisibility(View view, boolean accessibilityEnabled)59 public static void updateVisibility(View view, boolean accessibilityEnabled) { 60 // We want to avoid the extra layout pass by setting the views to GONE unless 61 // accessibility is on, in which case not setting them to GONE causes a glitch. 62 int invisibleState = accessibilityEnabled ? View.GONE : View.INVISIBLE; 63 if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) { 64 view.setVisibility(invisibleState); 65 } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD 66 && view.getVisibility() != View.VISIBLE) { 67 view.setVisibility(View.VISIBLE); 68 } 69 } 70 71 @Override onAnimationCancel(Animator animation)72 public void onAnimationCancel(Animator animation) { 73 mCanceled = true; 74 } 75 76 @Override onAnimationEnd(Animator arg0)77 public void onAnimationEnd(Animator arg0) { 78 if (mCanceled) return; 79 updateVisibility(mView, mAccessibilityEnabled); 80 } 81 82 @Override onAnimationStart(Animator arg0)83 public void onAnimationStart(Animator arg0) { 84 // We want the views to be visible for animation, so fade-in/out is visible 85 mView.setVisibility(View.VISIBLE); 86 } 87 } 88 89 /** 90 * This interpolator emulates the rate at which the perceived scale of an object changes 91 * as its distance from a camera increases. When this interpolator is applied to a scale 92 * animation on a view, it evokes the sense that the object is shrinking due to moving away 93 * from the camera. 94 */ 95 class ZInterpolator implements TimeInterpolator { 96 private float focalLength; 97 ZInterpolator(float foc)98 public ZInterpolator(float foc) { 99 focalLength = foc; 100 } 101 getInterpolation(float input)102 public float getInterpolation(float input) { 103 return (1.0f - focalLength / (focalLength + input)) / 104 (1.0f - focalLength / (focalLength + 1.0f)); 105 } 106 } 107 108 /** 109 * The exact reverse of ZInterpolator. 110 */ 111 class InverseZInterpolator implements TimeInterpolator { 112 private ZInterpolator zInterpolator; InverseZInterpolator(float foc)113 public InverseZInterpolator(float foc) { 114 zInterpolator = new ZInterpolator(foc); 115 } getInterpolation(float input)116 public float getInterpolation(float input) { 117 return 1 - zInterpolator.getInterpolation(1 - input); 118 } 119 } 120 121 /** 122 * InverseZInterpolator compounded with an ease-out. 123 */ 124 class ZoomInInterpolator implements TimeInterpolator { 125 private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f); 126 private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f); 127 getInterpolation(float input)128 public float getInterpolation(float input) { 129 return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input)); 130 } 131 } 132 133 /** 134 * Stores the transition states for convenience. 135 */ 136 class TransitionStates { 137 138 // Raw states 139 final boolean oldStateIsNormal; 140 final boolean oldStateIsSpringLoaded; 141 final boolean oldStateIsNormalHidden; 142 final boolean oldStateIsOverviewHidden; 143 final boolean oldStateIsOverview; 144 145 final boolean stateIsNormal; 146 final boolean stateIsSpringLoaded; 147 final boolean stateIsNormalHidden; 148 final boolean stateIsOverviewHidden; 149 final boolean stateIsOverview; 150 151 // Convenience members 152 final boolean workspaceToAllApps; 153 final boolean overviewToAllApps; 154 final boolean allAppsToWorkspace; 155 final boolean workspaceToOverview; 156 final boolean overviewToWorkspace; 157 TransitionStates(final Workspace.State fromState, final Workspace.State toState)158 public TransitionStates(final Workspace.State fromState, final Workspace.State toState) { 159 oldStateIsNormal = (fromState == Workspace.State.NORMAL); 160 oldStateIsSpringLoaded = (fromState == Workspace.State.SPRING_LOADED); 161 oldStateIsNormalHidden = (fromState == Workspace.State.NORMAL_HIDDEN); 162 oldStateIsOverviewHidden = (fromState == Workspace.State.OVERVIEW_HIDDEN); 163 oldStateIsOverview = (fromState == Workspace.State.OVERVIEW); 164 165 stateIsNormal = (toState == Workspace.State.NORMAL); 166 stateIsSpringLoaded = (toState == Workspace.State.SPRING_LOADED); 167 stateIsNormalHidden = (toState == Workspace.State.NORMAL_HIDDEN); 168 stateIsOverviewHidden = (toState == Workspace.State.OVERVIEW_HIDDEN); 169 stateIsOverview = (toState == Workspace.State.OVERVIEW); 170 171 workspaceToOverview = (oldStateIsNormal && stateIsOverview); 172 workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden); 173 overviewToWorkspace = (oldStateIsOverview && stateIsNormal); 174 overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden); 175 allAppsToWorkspace = (oldStateIsNormalHidden && stateIsNormal); 176 } 177 } 178 179 /** 180 * Manages the animations between each of the workspace states. 181 */ 182 public class WorkspaceStateTransitionAnimation { 183 184 public static final String TAG = "WorkspaceStateTransitionAnimation"; 185 186 @Thunk static final int BACKGROUND_FADE_OUT_DURATION = 350; 187 188 final @Thunk Launcher mLauncher; 189 final @Thunk Workspace mWorkspace; 190 191 @Thunk AnimatorSet mStateAnimator; 192 193 @Thunk float mCurrentScale; 194 @Thunk float mNewScale; 195 196 @Thunk final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator(); 197 198 @Thunk float mSpringLoadedShrinkFactor; 199 @Thunk float mOverviewModeShrinkFactor; 200 @Thunk float mWorkspaceScrimAlpha; 201 @Thunk int mAllAppsTransitionTime; 202 @Thunk int mOverviewTransitionTime; 203 @Thunk int mOverlayTransitionTime; 204 @Thunk int mSpringLoadedTransitionTime; 205 @Thunk boolean mWorkspaceFadeInAdjacentScreens; 206 WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace)207 public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) { 208 mLauncher = launcher; 209 mWorkspace = workspace; 210 211 DeviceProfile grid = mLauncher.getDeviceProfile(); 212 Resources res = launcher.getResources(); 213 mAllAppsTransitionTime = res.getInteger(R.integer.config_allAppsTransitionTime); 214 mOverviewTransitionTime = res.getInteger(R.integer.config_overviewTransitionTime); 215 mOverlayTransitionTime = res.getInteger(R.integer.config_overlayTransitionTime); 216 mSpringLoadedTransitionTime = mOverlayTransitionTime / 2; 217 mSpringLoadedShrinkFactor = mLauncher.getDeviceProfile().workspaceSpringLoadShrinkFactor; 218 mOverviewModeShrinkFactor = 219 res.getInteger(R.integer.config_workspaceOverviewShrinkPercentage) / 100f; 220 mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f; 221 mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens(); 222 } 223 snapToPageFromOverView(int whichPage)224 public void snapToPageFromOverView(int whichPage) { 225 mWorkspace.snapToPage(whichPage, mOverviewTransitionTime, mZoomInInterpolator); 226 } 227 getAnimationToState(Workspace.State fromState, Workspace.State toState, boolean animated, AnimationLayerSet layerViews)228 public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState, 229 boolean animated, AnimationLayerSet layerViews) { 230 AccessibilityManager am = (AccessibilityManager) 231 mLauncher.getSystemService(Context.ACCESSIBILITY_SERVICE); 232 final boolean accessibilityEnabled = am.isEnabled(); 233 TransitionStates states = new TransitionStates(fromState, toState); 234 int workspaceDuration = getAnimationDuration(states); 235 animateWorkspace(states, animated, workspaceDuration, layerViews, 236 accessibilityEnabled); 237 animateBackgroundGradient(states, animated, BACKGROUND_FADE_OUT_DURATION); 238 return mStateAnimator; 239 } 240 getFinalScale()241 public float getFinalScale() { 242 return mNewScale; 243 } 244 245 /** 246 * Returns the proper animation duration for a transition. 247 */ getAnimationDuration(TransitionStates states)248 private int getAnimationDuration(TransitionStates states) { 249 if (states.workspaceToAllApps || states.overviewToAllApps) { 250 return mAllAppsTransitionTime; 251 } else if (states.workspaceToOverview || states.overviewToWorkspace) { 252 return mOverviewTransitionTime; 253 } else if (mLauncher.mState == Launcher.State.WORKSPACE_SPRING_LOADED 254 || states.oldStateIsNormal && states.stateIsSpringLoaded) { 255 return mSpringLoadedTransitionTime; 256 } else { 257 return mOverlayTransitionTime; 258 } 259 } 260 261 /** 262 * Starts a transition animation for the workspace. 263 */ animateWorkspace(final TransitionStates states, final boolean animated, final int duration, AnimationLayerSet layerViews, final boolean accessibilityEnabled)264 private void animateWorkspace(final TransitionStates states, final boolean animated, 265 final int duration, AnimationLayerSet layerViews, final boolean accessibilityEnabled) { 266 // Cancel existing workspace animations and create a new animator set if requested 267 cancelAnimation(); 268 if (animated) { 269 mStateAnimator = LauncherAnimUtils.createAnimatorSet(); 270 } 271 272 // Update the workspace state 273 float finalBackgroundAlpha = (states.stateIsSpringLoaded || states.stateIsOverview) ? 274 1.0f : 0f; 275 float finalHotseatAlpha = (states.stateIsNormal || states.stateIsSpringLoaded || 276 (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && states.stateIsNormalHidden)) ? 1f : 0f; 277 float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f; 278 float finalQsbAlpha = (states.stateIsNormal || 279 (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && states.stateIsNormalHidden)) ? 1f : 0f; 280 281 float finalWorkspaceTranslationY = 0; 282 if (states.stateIsOverview || states.stateIsOverviewHidden) { 283 finalWorkspaceTranslationY = mWorkspace.getOverviewModeTranslationY(); 284 } else if (states.stateIsSpringLoaded) { 285 finalWorkspaceTranslationY = mWorkspace.getSpringLoadedTranslationY(); 286 } 287 288 final int childCount = mWorkspace.getChildCount(); 289 final int customPageCount = mWorkspace.numCustomPages(); 290 291 mNewScale = 1.0f; 292 293 if (states.oldStateIsOverview) { 294 mWorkspace.disableFreeScroll(); 295 } else if (states.stateIsOverview) { 296 mWorkspace.enableFreeScroll(); 297 } 298 299 if (!states.stateIsNormal) { 300 if (states.stateIsSpringLoaded) { 301 mNewScale = mSpringLoadedShrinkFactor; 302 } else if (states.stateIsOverview || states.stateIsOverviewHidden) { 303 mNewScale = mOverviewModeShrinkFactor; 304 } 305 } 306 307 int toPage = mWorkspace.getPageNearestToCenterOfScreen(); 308 // TODO: Animate the celllayout alpha instead of the pages. 309 for (int i = 0; i < childCount; i++) { 310 final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); 311 float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); 312 float finalAlpha; 313 if (states.stateIsOverviewHidden) { 314 finalAlpha = 0f; 315 } else if(states.stateIsNormalHidden) { 316 finalAlpha = (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && 317 i == mWorkspace.getNextPage()) ? 1 : 0; 318 } else if (states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) { 319 finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f; 320 } else { 321 finalAlpha = 1f; 322 } 323 324 // If we are animating to/from the small state, then hide the side pages and fade the 325 // current page in 326 if (!FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && !mWorkspace.isSwitchingState()) { 327 if (states.workspaceToAllApps || states.allAppsToWorkspace) { 328 boolean isCurrentPage = (i == toPage); 329 if (states.allAppsToWorkspace && isCurrentPage) { 330 initialAlpha = 0f; 331 } else if (!isCurrentPage) { 332 initialAlpha = finalAlpha = 0f; 333 } 334 cl.setShortcutAndWidgetAlpha(initialAlpha); 335 } 336 } 337 338 if (animated) { 339 float oldBackgroundAlpha = cl.getBackgroundAlpha(); 340 if (initialAlpha != finalAlpha) { 341 Animator alphaAnim = ObjectAnimator.ofFloat( 342 cl.getShortcutsAndWidgets(), View.ALPHA, finalAlpha); 343 alphaAnim.setDuration(duration) 344 .setInterpolator(mZoomInInterpolator); 345 mStateAnimator.play(alphaAnim); 346 } 347 if (oldBackgroundAlpha != 0 || finalBackgroundAlpha != 0) { 348 ValueAnimator bgAnim = ObjectAnimator.ofFloat(cl, "backgroundAlpha", 349 oldBackgroundAlpha, finalBackgroundAlpha); 350 bgAnim.setInterpolator(mZoomInInterpolator); 351 bgAnim.setDuration(duration); 352 mStateAnimator.play(bgAnim); 353 } 354 } else { 355 cl.setBackgroundAlpha(finalBackgroundAlpha); 356 cl.setShortcutAndWidgetAlpha(finalAlpha); 357 } 358 359 if (Workspace.isQsbContainerPage(i) && 360 states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) { 361 if (animated) { 362 Animator anim = mWorkspace.mQsbAlphaController 363 .animateAlphaAtIndex(finalAlpha, Workspace.QSB_ALPHA_INDEX_PAGE_SCROLL); 364 anim.setDuration(duration); 365 anim.setInterpolator(mZoomInInterpolator); 366 mStateAnimator.play(anim); 367 } else { 368 mWorkspace.mQsbAlphaController.setAlphaAtIndex( 369 finalAlpha, Workspace.QSB_ALPHA_INDEX_PAGE_SCROLL); 370 } 371 } 372 } 373 374 final ViewGroup overviewPanel = mLauncher.getOverviewPanel(); 375 376 Animator qsbAlphaAnimation = mWorkspace.mQsbAlphaController 377 .animateAlphaAtIndex(finalQsbAlpha, Workspace.QSB_ALPHA_INDEX_STATE_CHANGE); 378 379 if (animated) { 380 Animator scale = LauncherAnimUtils.ofPropertyValuesHolder(mWorkspace, 381 new PropertyListBuilder().scale(mNewScale) 382 .translationY(finalWorkspaceTranslationY).build()) 383 .setDuration(duration); 384 scale.setInterpolator(mZoomInInterpolator); 385 mStateAnimator.play(scale); 386 Animator hotseatAlpha = mWorkspace.createHotseatAlphaAnimator(finalHotseatAlpha); 387 388 Animator overviewPanelAlpha = ObjectAnimator.ofFloat( 389 overviewPanel, View.ALPHA, finalOverviewPanelAlpha); 390 overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel, 391 accessibilityEnabled)); 392 393 // For animation optimization, we may need to provide the Launcher transition 394 // with a set of views on which to force build and manage layers in certain scenarios. 395 layerViews.addView(overviewPanel); 396 layerViews.addView(mLauncher.getQsbContainer()); 397 layerViews.addView(mLauncher.getHotseat()); 398 layerViews.addView(mWorkspace.getPageIndicator()); 399 400 if (states.workspaceToOverview) { 401 hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); 402 overviewPanelAlpha.setInterpolator(null); 403 } else if (states.overviewToWorkspace) { 404 hotseatAlpha.setInterpolator(null); 405 overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); 406 } 407 408 overviewPanelAlpha.setDuration(duration); 409 hotseatAlpha.setDuration(duration); 410 qsbAlphaAnimation.setDuration(duration); 411 412 mStateAnimator.play(overviewPanelAlpha); 413 mStateAnimator.play(hotseatAlpha); 414 mStateAnimator.play(qsbAlphaAnimation); 415 mStateAnimator.addListener(new AnimatorListenerAdapter() { 416 boolean canceled = false; 417 @Override 418 public void onAnimationCancel(Animator animation) { 419 canceled = true; 420 } 421 422 @Override 423 public void onAnimationStart(Animator animation) { 424 mWorkspace.getPageIndicator().setShouldAutoHide(!states.stateIsSpringLoaded); 425 } 426 427 @Override 428 public void onAnimationEnd(Animator animation) { 429 mStateAnimator = null; 430 if (canceled) return; 431 if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { 432 overviewPanel.getChildAt(0).performAccessibilityAction( 433 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 434 } 435 } 436 }); 437 } else { 438 overviewPanel.setAlpha(finalOverviewPanelAlpha); 439 AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled); 440 mWorkspace.getPageIndicator().setShouldAutoHide(!states.stateIsSpringLoaded); 441 442 qsbAlphaAnimation.end(); 443 mWorkspace.createHotseatAlphaAnimator(finalHotseatAlpha).end(); 444 mWorkspace.updateCustomContentVisibility(); 445 mWorkspace.setScaleX(mNewScale); 446 mWorkspace.setScaleY(mNewScale); 447 mWorkspace.setTranslationY(finalWorkspaceTranslationY); 448 449 if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { 450 overviewPanel.getChildAt(0).performAccessibilityAction( 451 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 452 } 453 } 454 } 455 456 /** 457 * Animates the background scrim. Add to the state animator to prevent jankiness. 458 * 459 * @param states the current and final workspace states 460 * @param animated whether or not to set the background alpha immediately 461 * @duration duration of the animation 462 */ animateBackgroundGradient(TransitionStates states, boolean animated, int duration)463 private void animateBackgroundGradient(TransitionStates states, 464 boolean animated, int duration) { 465 466 final DragLayer dragLayer = mLauncher.getDragLayer(); 467 final float startAlpha = dragLayer.getBackgroundAlpha(); 468 float finalAlpha = states.stateIsNormal || states.stateIsNormalHidden ? 469 0 : mWorkspaceScrimAlpha; 470 471 if (finalAlpha != startAlpha) { 472 if (animated) { 473 // These properties refer to the background protection gradient used for AllApps 474 // and Widget tray. 475 ValueAnimator bgFadeOutAnimation = ValueAnimator.ofFloat(startAlpha, finalAlpha); 476 bgFadeOutAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 477 @Override 478 public void onAnimationUpdate(ValueAnimator animation) { 479 dragLayer.setBackgroundAlpha( 480 ((Float)animation.getAnimatedValue()).floatValue()); 481 } 482 }); 483 bgFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); 484 bgFadeOutAnimation.setDuration(duration); 485 mStateAnimator.play(bgFadeOutAnimation); 486 } else { 487 dragLayer.setBackgroundAlpha(finalAlpha); 488 } 489 } 490 } 491 492 /** 493 * Cancels the current animation. 494 */ cancelAnimation()495 private void cancelAnimation() { 496 if (mStateAnimator != null) { 497 mStateAnimator.setDuration(0); 498 mStateAnimator.cancel(); 499 } 500 mStateAnimator = null; 501 } 502 }