2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.launcher3;
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;
33 import com.android.launcher3.util.Thunk;
35 import java.util.HashMap;
38 * A convenience class to update a view's visibility state after an alpha animation.
40 class AlphaUpdateListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener {
41 private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
44 private boolean mAccessibilityEnabled;
46 public AlphaUpdateListener(View v, boolean accessibilityEnabled) {
48 mAccessibilityEnabled = accessibilityEnabled;
52 public void onAnimationUpdate(ValueAnimator arg0) {
53 updateVisibility(mView, mAccessibilityEnabled);
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);
69 public void onAnimationEnd(Animator arg0) {
70 updateVisibility(mView, mAccessibilityEnabled);
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);
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
86 class ZInterpolator implements TimeInterpolator {
87 private float focalLength;
89 public ZInterpolator(float foc) {
93 public float getInterpolation(float input) {
94 return (1.0f - focalLength / (focalLength + input)) /
95 (1.0f - focalLength / (focalLength + 1.0f));
100 * The exact reverse of ZInterpolator.
102 class InverseZInterpolator implements TimeInterpolator {
103 private ZInterpolator zInterpolator;
104 public InverseZInterpolator(float foc) {
105 zInterpolator = new ZInterpolator(foc);
107 public float getInterpolation(float input) {
108 return 1 - zInterpolator.getInterpolation(1 - input);
113 * InverseZInterpolator compounded with an ease-out.
115 class ZoomInInterpolator implements TimeInterpolator {
116 private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f);
117 private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f);
119 public float getInterpolation(float input) {
120 return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input));
125 * Stores the transition states for convenience.
127 class TransitionStates {
130 final boolean oldStateIsNormal;
131 final boolean oldStateIsSpringLoaded;
132 final boolean oldStateIsNormalHidden;
133 final boolean oldStateIsOverviewHidden;
134 final boolean oldStateIsOverview;
136 final boolean stateIsNormal;
137 final boolean stateIsSpringLoaded;
138 final boolean stateIsNormalHidden;
139 final boolean stateIsOverviewHidden;
140 final boolean stateIsOverview;
142 // Convenience members
143 final boolean workspaceToAllApps;
144 final boolean overviewToAllApps;
145 final boolean allAppsToWorkspace;
146 final boolean workspaceToOverview;
147 final boolean overviewToWorkspace;
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);
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);
162 workspaceToOverview = (oldStateIsNormal && stateIsOverview);
163 workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden);
164 overviewToWorkspace = (oldStateIsOverview && stateIsNormal);
165 overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden);
166 allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal);
171 * Manages the animations between each of the workspace states.
173 public class WorkspaceStateTransitionAnimation {
175 public static final String TAG = "WorkspaceStateTransitionAnimation";
177 public static final int SCROLL_TO_CURRENT_PAGE = -1;
178 @Thunk static final int BACKGROUND_FADE_OUT_DURATION = 350;
180 final @Thunk Launcher mLauncher;
181 final @Thunk Workspace mWorkspace;
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;
190 @Thunk float mCurrentScale;
191 @Thunk float mNewScale;
193 @Thunk final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator();
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;
203 public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) {
204 mLauncher = launcher;
205 mWorkspace = workspace;
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 mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f;
215 mOverviewModeShrinkFactor = grid.getOverviewModeScale(Utilities.isRtl(res));
216 mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
219 public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState,
220 int toPage, boolean animated, boolean hasOverlaySearchBar,
221 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 duration = getAnimationDuration(states);
227 animateWorkspace(states, toPage, animated, duration, layerViews,
228 accessibilityEnabled);
229 animateSearchBar(states, animated, duration, hasOverlaySearchBar, layerViews,
230 accessibilityEnabled);
231 animateBackgroundGradient(states, animated, BACKGROUND_FADE_OUT_DURATION);
232 return mStateAnimator;
235 public float getFinalScale() {
240 * Reinitializes the arrays that we need for the animations on each page.
242 private void reinitializeAnimationArrays() {
243 final int childCount = mWorkspace.getChildCount();
244 if (mLastChildCount == childCount) return;
246 mOldBackgroundAlphas = new float[childCount];
247 mOldAlphas = new float[childCount];
248 mNewBackgroundAlphas = new float[childCount];
249 mNewAlphas = new float[childCount];
253 * Returns the proper animation duration for a transition.
255 private int getAnimationDuration(TransitionStates states) {
256 if (states.workspaceToAllApps || states.overviewToAllApps) {
257 return mAllAppsTransitionTime;
258 } else if (states.workspaceToOverview || states.overviewToWorkspace) {
259 return mOverviewTransitionTime;
261 return mOverlayTransitionTime;
266 * Starts a transition animation for the workspace.
268 private void animateWorkspace(final TransitionStates states, int toPage, final boolean animated,
269 final int duration, final HashMap<View, Integer> layerViews,
270 final boolean accessibilityEnabled) {
271 // Reinitialize animation arrays for the current workspace state
272 reinitializeAnimationArrays();
274 // Cancel existing workspace animations and create a new animator set if requested
277 mStateAnimator = LauncherAnimUtils.createAnimatorSet();
280 // Update the workspace state
281 float finalBackgroundAlpha = (states.stateIsSpringLoaded || states.stateIsOverview) ?
283 float finalHotseatAndPageIndicatorAlpha = (states.stateIsNormal || states.stateIsSpringLoaded) ?
285 float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f;
286 float finalWorkspaceTranslationY = states.stateIsOverview || states.stateIsOverviewHidden ?
287 mWorkspace.getOverviewModeTranslationY() : 0;
289 final int childCount = mWorkspace.getChildCount();
290 final int customPageCount = mWorkspace.numCustomPages();
294 if (states.oldStateIsOverview) {
295 mWorkspace.disableFreeScroll();
296 } else if (states.stateIsOverview) {
297 mWorkspace.enableFreeScroll();
300 if (!states.stateIsNormal) {
301 if (states.stateIsSpringLoaded) {
302 mNewScale = mSpringLoadedShrinkFactor;
303 } else if (states.stateIsOverview || states.stateIsOverviewHidden) {
304 mNewScale = mOverviewModeShrinkFactor;
308 if (toPage == SCROLL_TO_CURRENT_PAGE) {
309 toPage = mWorkspace.getPageNearestToCenterOfScreen();
311 mWorkspace.snapToPage(toPage, duration, mZoomInInterpolator);
313 for (int i = 0; i < childCount; i++) {
314 final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i);
315 boolean isCurrentPage = (i == toPage);
316 float initialAlpha = cl.getShortcutsAndWidgets().getAlpha();
318 if (states.stateIsNormalHidden || states.stateIsOverviewHidden) {
320 } else if (states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) {
321 finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f;
326 // If we are animating to/from the small state, then hide the side pages and fade the
328 if (!mWorkspace.isSwitchingState()) {
329 if (states.workspaceToAllApps || states.allAppsToWorkspace) {
330 if (states.allAppsToWorkspace && isCurrentPage) {
332 } else if (!isCurrentPage) {
333 initialAlpha = finalAlpha = 0f;
335 cl.setShortcutAndWidgetAlpha(initialAlpha);
339 mOldAlphas[i] = initialAlpha;
340 mNewAlphas[i] = finalAlpha;
342 mOldBackgroundAlphas[i] = cl.getBackgroundAlpha();
343 mNewBackgroundAlphas[i] = finalBackgroundAlpha;
345 cl.setBackgroundAlpha(finalBackgroundAlpha);
346 cl.setShortcutAndWidgetAlpha(finalAlpha);
350 final ViewGroup overviewPanel = mLauncher.getOverviewPanel();
351 final View hotseat = mLauncher.getHotseat();
352 final View pageIndicator = mWorkspace.getPageIndicator();
354 LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mWorkspace);
355 scale.scaleX(mNewScale)
357 .translationY(finalWorkspaceTranslationY)
358 .setDuration(duration)
359 .setInterpolator(mZoomInInterpolator);
360 mStateAnimator.play(scale);
361 for (int index = 0; index < childCount; index++) {
363 final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i);
364 float currentAlpha = cl.getShortcutsAndWidgets().getAlpha();
365 if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) {
366 cl.setBackgroundAlpha(mNewBackgroundAlphas[i]);
367 cl.setShortcutAndWidgetAlpha(mNewAlphas[i]);
369 if (layerViews != null) {
370 layerViews.put(cl, LauncherStateTransitionAnimation.BUILD_LAYER);
372 if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) {
373 LauncherViewPropertyAnimator alphaAnim =
374 new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets());
375 alphaAnim.alpha(mNewAlphas[i])
376 .setDuration(duration)
377 .setInterpolator(mZoomInInterpolator);
378 mStateAnimator.play(alphaAnim);
380 if (mOldBackgroundAlphas[i] != 0 ||
381 mNewBackgroundAlphas[i] != 0) {
382 ValueAnimator bgAnim = ObjectAnimator.ofFloat(cl, "backgroundAlpha",
383 mOldBackgroundAlphas[i], mNewBackgroundAlphas[i]);
384 LauncherAnimUtils.ofFloat(cl, 0f, 1f);
385 bgAnim.setInterpolator(mZoomInInterpolator);
386 bgAnim.setDuration(duration);
387 mStateAnimator.play(bgAnim);
391 Animator pageIndicatorAlpha;
392 if (pageIndicator != null) {
393 pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator)
394 .alpha(finalHotseatAndPageIndicatorAlpha).withLayer();
395 pageIndicatorAlpha.addListener(new AlphaUpdateListener(pageIndicator,
396 accessibilityEnabled));
398 // create a dummy animation so we don't need to do null checks later
399 pageIndicatorAlpha = ValueAnimator.ofFloat(0, 0);
402 LauncherViewPropertyAnimator hotseatAlpha = new LauncherViewPropertyAnimator(hotseat)
403 .alpha(finalHotseatAndPageIndicatorAlpha);
404 hotseatAlpha.addListener(new AlphaUpdateListener(hotseat, accessibilityEnabled));
406 LauncherViewPropertyAnimator overviewPanelAlpha =
407 new LauncherViewPropertyAnimator(overviewPanel).alpha(finalOverviewPanelAlpha);
408 overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel,
409 accessibilityEnabled));
411 // For animation optimations, we may need to provide the Launcher transition
412 // with a set of views on which to force build layers in certain scenarios.
413 hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null);
414 overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null);
415 if (layerViews != null) {
416 // If layerViews is not null, we add these views, and indicate that
417 // the caller can manage layer state.
418 layerViews.put(hotseat, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
419 layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
421 // Otherwise let the animator handle layer management.
422 hotseatAlpha.withLayer();
423 overviewPanelAlpha.withLayer();
426 if (states.workspaceToOverview) {
427 pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2));
428 hotseatAlpha.setInterpolator(new DecelerateInterpolator(2));
429 overviewPanelAlpha.setInterpolator(null);
430 } else if (states.overviewToWorkspace) {
431 pageIndicatorAlpha.setInterpolator(null);
432 hotseatAlpha.setInterpolator(null);
433 overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2));
436 overviewPanelAlpha.setDuration(duration);
437 pageIndicatorAlpha.setDuration(duration);
438 hotseatAlpha.setDuration(duration);
440 mStateAnimator.play(overviewPanelAlpha);
441 mStateAnimator.play(hotseatAlpha);
442 mStateAnimator.play(pageIndicatorAlpha);
443 mStateAnimator.addListener(new AnimatorListenerAdapter() {
445 public void onAnimationEnd(Animator animation) {
446 mStateAnimator = null;
448 if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) {
449 overviewPanel.getChildAt(0).performAccessibilityAction(
450 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
455 overviewPanel.setAlpha(finalOverviewPanelAlpha);
456 AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled);
457 hotseat.setAlpha(finalHotseatAndPageIndicatorAlpha);
458 AlphaUpdateListener.updateVisibility(hotseat, accessibilityEnabled);
459 if (pageIndicator != null) {
460 pageIndicator.setAlpha(finalHotseatAndPageIndicatorAlpha);
461 AlphaUpdateListener.updateVisibility(pageIndicator, accessibilityEnabled);
463 mWorkspace.updateCustomContentVisibility();
464 mWorkspace.setScaleX(mNewScale);
465 mWorkspace.setScaleY(mNewScale);
466 mWorkspace.setTranslationY(finalWorkspaceTranslationY);
468 if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) {
469 overviewPanel.getChildAt(0).performAccessibilityAction(
470 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
476 * Coordinates with the workspace animation to animate the search bar.
478 * TODO: This should really be coordinated with the SearchDropTargetBar, otherwise the
479 * bar has no idea that it is hidden, and this has no idea what state the bar is
482 private void animateSearchBar(TransitionStates states, boolean animated, int duration,
483 boolean hasOverlaySearchBar, final HashMap<View, Integer> layerViews,
484 final boolean accessibilityEnabled) {
486 // The search bar is only visible in the workspace
487 final View searchBar = mLauncher.getOrCreateQsbBar();
488 if (searchBar != null) {
489 final boolean searchBarWillBeShown = states.stateIsNormal;
490 final float finalSearchBarAlpha = searchBarWillBeShown ? 1f : 0f;
492 if (hasOverlaySearchBar) {
493 // If there is an overlay search bar, then we will coordinate with it.
494 mStateAnimator.addListener(new AnimatorListenerAdapter() {
496 public void onAnimationStart(Animator animation) {
497 // If we are transitioning to a visible search bar, show it immediately
498 // and let the overlay search bar has faded out
499 if (searchBarWillBeShown) {
500 searchBar.setAlpha(finalSearchBarAlpha);
501 AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled);
506 public void onAnimationEnd(Animator animation) {
507 // If we are transitioning to a hidden search bar, hide it only after
508 // the overlay search bar has faded in
509 if (!searchBarWillBeShown) {
510 searchBar.setAlpha(finalSearchBarAlpha);
511 AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled);
516 // Otherwise, we can just do the normal animation
517 LauncherViewPropertyAnimator searchBarAlpha =
518 new LauncherViewPropertyAnimator(searchBar).alpha(finalSearchBarAlpha);
519 searchBarAlpha.addListener(new AlphaUpdateListener(searchBar,
520 accessibilityEnabled));
521 searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null);
522 if (layerViews != null) {
523 // If layerViews is not null, we add these views, and indicate that
524 // the caller can manage layer state.
525 layerViews.put(searchBar, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
527 // Otherwise let the animator handle layer management.
528 searchBarAlpha.withLayer();
530 searchBarAlpha.setDuration(duration);
531 mStateAnimator.play(searchBarAlpha);
534 // Set the search bar state immediately
535 searchBar.setAlpha(finalSearchBarAlpha);
536 AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled);
542 * Animates the background scrim. Add to the state animator to prevent jankiness.
544 * @param finalAlpha the final alpha for the background scrim
545 * @param animated whether or not to set the background alpha immediately
546 * @duration duration of the animation
548 private void animateBackgroundGradient(TransitionStates states,
549 boolean animated, int duration) {
551 final DragLayer dragLayer = mLauncher.getDragLayer();
552 final float startAlpha = dragLayer.getBackgroundAlpha();
553 float finalAlpha = states.stateIsNormal ? 0 : mWorkspaceScrimAlpha;
555 if (finalAlpha != startAlpha) {
557 // These properties refer to the background protection gradient used for AllApps
559 ValueAnimator bgFadeOutAnimation =
560 LauncherAnimUtils.ofFloat(mWorkspace, startAlpha, finalAlpha);
561 bgFadeOutAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
563 public void onAnimationUpdate(ValueAnimator animation) {
564 dragLayer.setBackgroundAlpha(
565 ((Float)animation.getAnimatedValue()).floatValue());
568 bgFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f));
569 bgFadeOutAnimation.setDuration(duration);
570 mStateAnimator.play(bgFadeOutAnimation);
572 dragLayer.setBackgroundAlpha(finalAlpha);
578 * Cancels the current animation.
580 private void cancelAnimation() {
581 if (mStateAnimator != null) {
582 mStateAnimator.setDuration(0);
583 mStateAnimator.cancel();
585 mStateAnimator = null;