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 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, 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 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, 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 states.stateIsNormalHidden) ? 1f : 0f; 277 float finalQsbAlpha = (states.stateIsNormal || states.stateIsNormalHidden) ? 1f : 0f; 278 279 float finalWorkspaceTranslationY = 0; 280 if (states.stateIsOverview || states.stateIsOverviewHidden) { 281 finalWorkspaceTranslationY = mWorkspace.getOverviewModeTranslationY(); 282 } else if (states.stateIsSpringLoaded) { 283 finalWorkspaceTranslationY = mWorkspace.getSpringLoadedTranslationY(); 284 } 285 286 final int childCount = mWorkspace.getChildCount(); 287 final int customPageCount = mWorkspace.numCustomPages(); 288 289 mNewScale = 1.0f; 290 291 if (states.oldStateIsOverview) { 292 mWorkspace.disableFreeScroll(); 293 } else if (states.stateIsOverview) { 294 mWorkspace.enableFreeScroll(); 295 } 296 297 if (!states.stateIsNormal) { 298 if (states.stateIsSpringLoaded) { 299 mNewScale = mSpringLoadedShrinkFactor; 300 } else if (states.stateIsOverview || states.stateIsOverviewHidden) { 301 mNewScale = mOverviewModeShrinkFactor; 302 } 303 } 304 305 int toPage = mWorkspace.getPageNearestToCenterOfScreen(); 306 // TODO: Animate the celllayout alpha instead of the pages. 307 for (int i = 0; i < childCount; i++) { 308 final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); 309 float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); 310 float finalAlpha; 311 if (states.stateIsOverviewHidden) { 312 finalAlpha = 0f; 313 } else if(states.stateIsNormalHidden) { 314 finalAlpha = (i == mWorkspace.getNextPage()) ? 1 : 0; 315 } else if (states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) { 316 finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f; 317 } else { 318 finalAlpha = 1f; 319 } 320 321 // If we are animating to/from the small state, then hide the side pages and fade the 322 // current page in 323 if (!FeatureFlags.NO_ALL_APPS_ICON && !mWorkspace.isSwitchingState()) { 324 if (states.workspaceToAllApps || states.allAppsToWorkspace) { 325 boolean isCurrentPage = (i == toPage); 326 if (states.allAppsToWorkspace && isCurrentPage) { 327 initialAlpha = 0f; 328 } else if (!isCurrentPage) { 329 initialAlpha = finalAlpha = 0f; 330 } 331 cl.setShortcutAndWidgetAlpha(initialAlpha); 332 } 333 } 334 335 if (animated) { 336 float oldBackgroundAlpha = cl.getBackgroundAlpha(); 337 if (initialAlpha != finalAlpha) { 338 Animator alphaAnim = ObjectAnimator.ofFloat( 339 cl.getShortcutsAndWidgets(), View.ALPHA, finalAlpha); 340 alphaAnim.setDuration(duration) 341 .setInterpolator(mZoomInInterpolator); 342 mStateAnimator.play(alphaAnim); 343 } 344 if (oldBackgroundAlpha != 0 || finalBackgroundAlpha != 0) { 345 ValueAnimator bgAnim = ObjectAnimator.ofFloat(cl, "backgroundAlpha", 346 oldBackgroundAlpha, finalBackgroundAlpha); 347 bgAnim.setInterpolator(mZoomInInterpolator); 348 bgAnim.setDuration(duration); 349 mStateAnimator.play(bgAnim); 350 } 351 } else { 352 cl.setBackgroundAlpha(finalBackgroundAlpha); 353 cl.setShortcutAndWidgetAlpha(finalAlpha); 354 } 355 } 356 357 final ViewGroup overviewPanel = mLauncher.getOverviewPanel(); 358 359 float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f; 360 if (animated) { 361 // This is true when transitioning between: 362 // - Overview <-> Workspace 363 // - Overview <-> Widget Tray 364 if (finalOverviewPanelAlpha != overviewPanel.getAlpha()) { 365 Animator overviewPanelAlpha = ObjectAnimator.ofFloat( 366 overviewPanel, View.ALPHA, finalOverviewPanelAlpha); 367 overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel, 368 accessibilityEnabled)); 369 layerViews.addView(overviewPanel); 370 371 if (states.overviewToWorkspace) { 372 overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); 373 } else if (states.workspaceToOverview) { 374 overviewPanelAlpha.setInterpolator(null); 375 } 376 377 overviewPanelAlpha.setDuration(duration); 378 mStateAnimator.play(overviewPanelAlpha); 379 } 380 381 Animator scale = LauncherAnimUtils.ofPropertyValuesHolder(mWorkspace, 382 new PropertyListBuilder().scale(mNewScale) 383 .translationY(finalWorkspaceTranslationY).build()) 384 .setDuration(duration); 385 scale.setInterpolator(mZoomInInterpolator); 386 mStateAnimator.play(scale); 387 388 // For animation optimization, we may need to provide the Launcher transition 389 // with a set of views on which to force build and manage layers in certain scenarios. 390 layerViews.addView(mLauncher.getHotseat()); 391 layerViews.addView(mWorkspace.getPageIndicator()); 392 393 Animator hotseatAlpha = mWorkspace.createHotseatAlphaAnimator(finalHotseatAlpha); 394 if (states.workspaceToOverview) { 395 hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); 396 } else if (states.overviewToWorkspace) { 397 hotseatAlpha.setInterpolator(null); 398 } 399 hotseatAlpha.setDuration(duration); 400 mStateAnimator.play(hotseatAlpha); 401 mStateAnimator.addListener(new AnimatorListenerAdapter() { 402 boolean canceled = false; 403 @Override 404 public void onAnimationCancel(Animator animation) { 405 canceled = true; 406 } 407 408 @Override 409 public void onAnimationStart(Animator animation) { 410 mWorkspace.getPageIndicator().setShouldAutoHide(!states.stateIsSpringLoaded); 411 } 412 413 @Override 414 public void onAnimationEnd(Animator animation) { 415 mStateAnimator = null; 416 if (canceled) return; 417 if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { 418 overviewPanel.getChildAt(0).performAccessibilityAction( 419 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 420 } 421 } 422 }); 423 } else { 424 overviewPanel.setAlpha(finalOverviewPanelAlpha); 425 AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled); 426 mWorkspace.getPageIndicator().setShouldAutoHide(!states.stateIsSpringLoaded); 427 428 mWorkspace.createHotseatAlphaAnimator(finalHotseatAlpha).end(); 429 mWorkspace.updateCustomContentVisibility(); 430 mWorkspace.setScaleX(mNewScale); 431 mWorkspace.setScaleY(mNewScale); 432 mWorkspace.setTranslationY(finalWorkspaceTranslationY); 433 434 if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { 435 overviewPanel.getChildAt(0).performAccessibilityAction( 436 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 437 } 438 } 439 } 440 441 /** 442 * Animates the background scrim. Add to the state animator to prevent jankiness. 443 * 444 * @param states the current and final workspace states 445 * @param animated whether or not to set the background alpha immediately 446 * @duration duration of the animation 447 */ 448 private void animateBackgroundGradient(TransitionStates states, 449 boolean animated, int duration) { 450 451 final DragLayer dragLayer = mLauncher.getDragLayer(); 452 final float startAlpha = dragLayer.getBackgroundAlpha(); 453 float finalAlpha = states.stateIsNormal || states.stateIsNormalHidden ? 454 0 : mWorkspaceScrimAlpha; 455 456 if (finalAlpha != startAlpha) { 457 if (animated) { 458 // These properties refer to the background protection gradient used for AllApps 459 // and Widget tray. 460 ValueAnimator bgFadeOutAnimation = ValueAnimator.ofFloat(startAlpha, finalAlpha); 461 bgFadeOutAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 462 @Override 463 public void onAnimationUpdate(ValueAnimator animation) { 464 dragLayer.setBackgroundAlpha( 465 ((Float)animation.getAnimatedValue()).floatValue()); 466 } 467 }); 468 bgFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); 469 bgFadeOutAnimation.setDuration(duration); 470 mStateAnimator.play(bgFadeOutAnimation); 471 } else { 472 dragLayer.setBackgroundAlpha(finalAlpha); 473 } 474 } 475 } 476 477 /** 478 * Cancels the current animation. 479 */ 480 private void cancelAnimation() { 481 if (mStateAnimator != null) { 482 mStateAnimator.setDuration(0); 483 mStateAnimator.cancel(); 484 } 485 mStateAnimator = null; 486 } 487 }