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 }