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