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.config.FeatureFlags; 34 import com.android.launcher3.dragndrop.DragLayer; 35 import com.android.launcher3.util.Thunk; 36 37 import java.util.HashMap; 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 49 public AlphaUpdateListener(View v, boolean accessibilityEnabled) { 50 mView = v; 51 mAccessibilityEnabled = accessibilityEnabled; 52 } 53 54 @Override 55 public void onAnimationUpdate(ValueAnimator arg0) { 56 updateVisibility(mView, mAccessibilityEnabled); 57 } 58 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 72 public void onAnimationCancel(Animator animation) { 73 mCanceled = true; 74 } 75 76 @Override 77 public void onAnimationEnd(Animator arg0) { 78 if (mCanceled) return; 79 updateVisibility(mView, mAccessibilityEnabled); 80 } 81 82 @Override 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 98 public ZInterpolator(float foc) { 99 focalLength = foc; 100 } 101 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; 113 public InverseZInterpolator(float foc) { 114 zInterpolator = new ZInterpolator(foc); 115 } 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 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 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 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 224 public void snapToPageFromOverView(int whichPage) { 225 mWorkspace.snapToPage(whichPage, mOverviewTransitionTime, mZoomInInterpolator); 226 } 227 228 public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState, 229 boolean animated, HashMap<View, Integer> 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 241 public float getFinalScale() { 242 return mNewScale; 243 } 244 245 /** 246 * Returns the proper animation duration for a transition. 247 */ 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 */ 264 private void animateWorkspace(final TransitionStates states, final boolean animated, 265 final int duration, final HashMap<View, Integer> layerViews, 266 final boolean accessibilityEnabled) { 267 // Cancel existing workspace animations and create a new animator set if requested 268 cancelAnimation(); 269 if (animated) { 270 mStateAnimator = LauncherAnimUtils.createAnimatorSet(); 271 } 272 273 // Update the workspace state 274 float finalBackgroundAlpha = (states.stateIsSpringLoaded || states.stateIsOverview) ? 275 1.0f : 0f; 276 float finalHotseatAlpha = (states.stateIsNormal || states.stateIsSpringLoaded || 277 (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && states.stateIsNormalHidden)) ? 1f : 0f; 278 float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f; 279 float finalQsbAlpha = (states.stateIsNormal || 280 (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && states.stateIsNormalHidden)) ? 1f : 0f; 281 282 float finalWorkspaceTranslationY = 0; 283 if (states.stateIsOverview || states.stateIsOverviewHidden) { 284 finalWorkspaceTranslationY = mWorkspace.getOverviewModeTranslationY(); 285 } else if (states.stateIsSpringLoaded) { 286 finalWorkspaceTranslationY = mWorkspace.getSpringLoadedTranslationY(); 287 } 288 289 final int childCount = mWorkspace.getChildCount(); 290 final int customPageCount = mWorkspace.numCustomPages(); 291 292 mNewScale = 1.0f; 293 294 if (states.oldStateIsOverview) { 295 mWorkspace.disableFreeScroll(); 296 } else if (states.stateIsOverview) { 297 mWorkspace.enableFreeScroll(); 298 } 299 300 if (!states.stateIsNormal) { 301 if (states.stateIsSpringLoaded) { 302 mNewScale = mSpringLoadedShrinkFactor; 303 } else if (states.stateIsOverview || states.stateIsOverviewHidden) { 304 mNewScale = mOverviewModeShrinkFactor; 305 } 306 } 307 308 int toPage = mWorkspace.getPageNearestToCenterOfScreen(); 309 // TODO: Animate the celllayout alpha instead of the pages. 310 for (int i = 0; i < childCount; i++) { 311 final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); 312 float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); 313 float finalAlpha; 314 if (states.stateIsOverviewHidden) { 315 finalAlpha = 0f; 316 } else if(states.stateIsNormalHidden) { 317 finalAlpha = (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && 318 i == mWorkspace.getNextPage()) ? 1 : 0; 319 } else if (states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) { 320 finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f; 321 } else { 322 finalAlpha = 1f; 323 } 324 325 // If we are animating to/from the small state, then hide the side pages and fade the 326 // current page in 327 if (!FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && !mWorkspace.isSwitchingState()) { 328 if (states.workspaceToAllApps || states.allAppsToWorkspace) { 329 boolean isCurrentPage = (i == toPage); 330 if (states.allAppsToWorkspace && isCurrentPage) { 331 initialAlpha = 0f; 332 } else if (!isCurrentPage) { 333 initialAlpha = finalAlpha = 0f; 334 } 335 cl.setShortcutAndWidgetAlpha(initialAlpha); 336 } 337 } 338 339 if (animated) { 340 float oldBackgroundAlpha = cl.getBackgroundAlpha(); 341 if (initialAlpha != finalAlpha) { 342 LauncherViewPropertyAnimator alphaAnim = 343 new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets()); 344 alphaAnim.alpha(finalAlpha) 345 .setDuration(duration) 346 .setInterpolator(mZoomInInterpolator); 347 mStateAnimator.play(alphaAnim); 348 } 349 if (oldBackgroundAlpha != 0 || finalBackgroundAlpha != 0) { 350 ValueAnimator bgAnim = ObjectAnimator.ofFloat(cl, "backgroundAlpha", 351 oldBackgroundAlpha, finalBackgroundAlpha); 352 bgAnim.setInterpolator(mZoomInInterpolator); 353 bgAnim.setDuration(duration); 354 mStateAnimator.play(bgAnim); 355 } 356 } else { 357 cl.setBackgroundAlpha(finalBackgroundAlpha); 358 cl.setShortcutAndWidgetAlpha(finalAlpha); 359 } 360 361 if (Workspace.isQsbContainerPage(i)) { 362 if (animated) { 363 Animator anim = mWorkspace.mQsbAlphaController 364 .animateAlphaAtIndex(finalAlpha, Workspace.QSB_ALPHA_INDEX_PAGE_SCROLL); 365 anim.setDuration(duration); 366 anim.setInterpolator(mZoomInInterpolator); 367 mStateAnimator.play(anim); 368 } else { 369 mWorkspace.mQsbAlphaController.setAlphaAtIndex( 370 finalAlpha, Workspace.QSB_ALPHA_INDEX_PAGE_SCROLL); 371 } 372 } 373 } 374 375 final ViewGroup overviewPanel = mLauncher.getOverviewPanel(); 376 377 final View qsbContainer = mLauncher.getQsbContainer(); 378 379 Animator qsbAlphaAnimation = mWorkspace.mQsbAlphaController 380 .animateAlphaAtIndex(finalQsbAlpha, Workspace.QSB_ALPHA_INDEX_STATE_CHANGE); 381 382 if (animated) { 383 LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mWorkspace); 384 scale.scaleX(mNewScale) 385 .scaleY(mNewScale) 386 .translationY(finalWorkspaceTranslationY) 387 .setDuration(duration) 388 .setInterpolator(mZoomInInterpolator); 389 mStateAnimator.play(scale); 390 Animator hotseatAlpha = mWorkspace.createHotseatAlphaAnimator(finalHotseatAlpha); 391 392 LauncherViewPropertyAnimator overviewPanelAlpha = 393 new LauncherViewPropertyAnimator(overviewPanel).alpha(finalOverviewPanelAlpha); 394 overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel, 395 accessibilityEnabled)); 396 397 // For animation optimization, we may need to provide the Launcher transition 398 // with a set of views on which to force build and manage layers in certain scenarios. 399 layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); 400 layerViews.put(qsbContainer, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); 401 layerViews.put(mLauncher.getHotseat(), 402 LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); 403 layerViews.put(mWorkspace.getPageIndicator(), 404 LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); 405 406 if (states.workspaceToOverview) { 407 hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); 408 overviewPanelAlpha.setInterpolator(null); 409 } else if (states.overviewToWorkspace) { 410 hotseatAlpha.setInterpolator(null); 411 overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); 412 } 413 414 overviewPanelAlpha.setDuration(duration); 415 hotseatAlpha.setDuration(duration); 416 qsbAlphaAnimation.setDuration(duration); 417 418 mStateAnimator.play(overviewPanelAlpha); 419 mStateAnimator.play(hotseatAlpha); 420 mStateAnimator.play(qsbAlphaAnimation); 421 mStateAnimator.addListener(new AnimatorListenerAdapter() { 422 boolean canceled = false; 423 @Override 424 public void onAnimationCancel(Animator animation) { 425 canceled = true; 426 } 427 428 @Override 429 public void onAnimationEnd(Animator animation) { 430 mStateAnimator = null; 431 if (canceled) return; 432 if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { 433 overviewPanel.getChildAt(0).performAccessibilityAction( 434 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 435 } 436 } 437 }); 438 } else { 439 overviewPanel.setAlpha(finalOverviewPanelAlpha); 440 AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled); 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 */ 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 */ 495 private void cancelAnimation() { 496 if (mStateAnimator != null) { 497 mStateAnimator.setDuration(0); 498 mStateAnimator.cancel(); 499 } 500 mStateAnimator = null; 501 } 502 }