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 package com.android.launcher3.allapps;
17 
18 import static com.android.app.animation.Interpolators.DECELERATE_1_7;
19 import static com.android.app.animation.Interpolators.INSTANT;
20 import static com.android.app.animation.Interpolators.LINEAR;
21 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
22 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
23 import static com.android.launcher3.LauncherState.ALL_APPS;
24 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
25 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
26 import static com.android.launcher3.LauncherState.NORMAL;
27 import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
28 import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
29 import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
30 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
31 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_BOTTOM_SHEET_FADE;
32 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
33 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
34 import static com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV;
35 import static com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV;
36 import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
37 
38 import android.animation.Animator;
39 import android.animation.ObjectAnimator;
40 import android.animation.ValueAnimator;
41 import android.util.FloatProperty;
42 import android.view.HapticFeedbackConstants;
43 import android.view.View;
44 import android.view.animation.Interpolator;
45 
46 import androidx.annotation.FloatRange;
47 import androidx.annotation.Nullable;
48 
49 import com.android.app.animation.Interpolators;
50 import com.android.launcher3.DeviceProfile;
51 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
52 import com.android.launcher3.Launcher;
53 import com.android.launcher3.LauncherState;
54 import com.android.launcher3.R;
55 import com.android.launcher3.Utilities;
56 import com.android.launcher3.anim.AnimatedFloat;
57 import com.android.launcher3.anim.PendingAnimation;
58 import com.android.launcher3.anim.PropertySetter;
59 import com.android.launcher3.config.FeatureFlags;
60 import com.android.launcher3.statemanager.StateManager.StateHandler;
61 import com.android.launcher3.states.StateAnimationConfig;
62 import com.android.launcher3.touch.AllAppsSwipeController;
63 import com.android.launcher3.util.MultiPropertyFactory;
64 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
65 import com.android.launcher3.util.MultiValueAlpha;
66 import com.android.launcher3.util.ScrollableLayoutManager;
67 import com.android.launcher3.util.Themes;
68 import com.android.launcher3.util.VibratorWrapper;
69 import com.android.launcher3.views.ScrimView;
70 
71 /**
72  * Handles AllApps view transition.
73  * 1) Slides all apps view using direct manipulation
74  * 2) When finger is released, animate to either top or bottom accordingly.
75  * <p/>
76  * Algorithm:
77  * If release velocity > THRES1, snap according to the direction of movement.
78  * If release velocity < THRES1, snap according to either top or bottom depending on whether it's
79  * closer to top or closer to the page indicator.
80  */
81 public class AllAppsTransitionController
82         implements StateHandler<LauncherState>, OnDeviceProfileChangeListener {
83     // This constant should match the second derivative of the animator interpolator.
84     public static final float INTERP_COEFF = 1.7f;
85     public static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200;
86 
87     private static final float NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.1f;
88     private static final float SWIPE_DRAG_COMMIT_THRESHOLD =
89             1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL;
90 
91     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
92             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
93 
94                 @Override
95                 public Float get(AllAppsTransitionController controller) {
96                     return controller.mProgress;
97                 }
98 
99                 @Override
100                 public void setValue(AllAppsTransitionController controller, float progress) {
101                     controller.setProgress(progress);
102                 }
103             };
104 
105     private static final float ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT = 0f;
106 
107     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PULL_BACK_TRANSLATION =
108             new FloatProperty<AllAppsTransitionController>("allAppsPullBackTranslation") {
109 
110                 @Override
111                 public Float get(AllAppsTransitionController controller) {
112                     if (controller.mIsTablet) {
113                         return controller.mAppsView.getActiveRecyclerView().getTranslationY();
114                     } else {
115                         return controller.getAppsViewPullbackTranslationY().getValue();
116                     }
117                 }
118 
119                 @Override
120                 public void setValue(AllAppsTransitionController controller, float translation) {
121                     if (controller.mIsTablet) {
122                         controller.mAppsView.getActiveRecyclerView().setTranslationY(translation);
123                         controller.getAppsViewPullbackTranslationY().setValue(
124                                 ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT);
125                     } else {
126                         controller.getAppsViewPullbackTranslationY().setValue(translation);
127                         controller.mAppsView.getActiveRecyclerView().setTranslationY(
128                                 ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT);
129                     }
130                 }
131             };
132 
133     private static final float ALL_APPS_PULL_BACK_ALPHA_DEFAULT = 1f;
134 
135     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PULL_BACK_ALPHA =
136             new FloatProperty<AllAppsTransitionController>("allAppsPullBackAlpha") {
137 
138                 @Override
139                 public Float get(AllAppsTransitionController controller) {
140                     if (controller.mIsTablet) {
141                         return controller.mAppsView.getActiveRecyclerView().getAlpha();
142                     } else {
143                         return controller.getAppsViewPullbackAlpha().getValue();
144                     }
145                 }
146 
147                 @Override
148                 public void setValue(AllAppsTransitionController controller, float alpha) {
149                     if (controller.mIsTablet) {
150                         controller.mAppsView.getActiveRecyclerView().setAlpha(alpha);
151                         controller.getAppsViewPullbackAlpha().setValue(
152                                 ALL_APPS_PULL_BACK_ALPHA_DEFAULT);
153                     } else {
154                         controller.getAppsViewPullbackAlpha().setValue(alpha);
155                         controller.mAppsView.getActiveRecyclerView().setAlpha(
156                                 ALL_APPS_PULL_BACK_ALPHA_DEFAULT);
157                     }
158                 }
159             };
160 
161     private static final int INDEX_APPS_VIEW_PROGRESS = 0;
162     private static final int INDEX_APPS_VIEW_PULLBACK = 1;
163     private static final int APPS_VIEW_INDEX_COUNT = 2;
164 
165     private ActivityAllAppsContainerView<Launcher> mAppsView;
166 
167     private final Launcher mLauncher;
168     private final AnimatedFloat mAllAppScale = new AnimatedFloat(this::onScaleProgressChanged);
169     private final int mNavScrimFlag;
170 
171     @Nullable private Animator.AnimatorListener mAllAppsSearchBackAnimationListener;
172 
173     private boolean mIsVerticalLayout;
174 
175     // Animation in this class is controlled by a single variable {@link mProgress}.
176     // Visually, it represents top y coordinate of the all apps container if multiplied with
177     // {@link mShiftRange}.
178 
179     // When {@link mProgress} is 0, all apps container is pulled up.
180     // When {@link mProgress} is 1, all apps container is pulled down.
181     private float mShiftRange;      // changes depending on the orientation
182     private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent
183 
184     private ScrimView mScrimView;
185 
186     private MultiValueAlpha mAppsViewAlpha;
187     private MultiPropertyFactory<View> mAppsViewTranslationY;
188 
189     private boolean mIsTablet;
190 
191     private boolean mHasScaleEffect;
192     private final VibratorWrapper mVibratorWrapper;
193 
AllAppsTransitionController(Launcher l)194     public AllAppsTransitionController(Launcher l) {
195         mLauncher = l;
196         DeviceProfile dp = mLauncher.getDeviceProfile();
197         mProgress = 1f;
198         mIsVerticalLayout = dp.isVerticalBarLayout();
199         mIsTablet = dp.isTablet;
200         mNavScrimFlag = Themes.getAttrBoolean(l, R.attr.isMainColorDark)
201                 ? FLAG_DARK_NAV : FLAG_LIGHT_NAV;
202 
203         setShiftRange(dp.allAppsShiftRange);
204         mAllAppScale.value = 1;
205         mLauncher.addOnDeviceProfileChangeListener(this);
206         mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext());
207     }
208 
getShiftRange()209     public float getShiftRange() {
210         return mShiftRange;
211     }
212 
213     @Override
onDeviceProfileChanged(DeviceProfile dp)214     public void onDeviceProfileChanged(DeviceProfile dp) {
215         mIsVerticalLayout = dp.isVerticalBarLayout();
216         setShiftRange(dp.allAppsShiftRange);
217 
218         if (mIsVerticalLayout) {
219             mLauncher.getHotseat().setTranslationY(0);
220             mLauncher.getWorkspace().getPageIndicator().setTranslationY(0);
221         }
222 
223         mIsTablet = dp.isTablet;
224     }
225 
226     /**
227      * Note this method should not be called outside this class. This is public because it is used
228      * in xml-based animations which also handle updating the appropriate UI.
229      *
230      * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace
231      * @see #setState(LauncherState)
232      * @see #setStateWithAnimation(LauncherState, StateAnimationConfig, PendingAnimation)
233      */
setProgress(float progress)234     public void setProgress(float progress) {
235         mProgress = progress;
236         boolean fromBackground =
237                 mLauncher.getStateManager().getCurrentStableState() == BACKGROUND_APP;
238         // Allow apps panel to shift the full screen if coming from another app.
239         float shiftRange = fromBackground ? mLauncher.getDeviceProfile().heightPx : mShiftRange;
240         getAppsViewProgressTranslationY().setValue(mProgress * shiftRange);
241         mLauncher.onAllAppsTransition(1 - progress);
242 
243         boolean hasScrim = progress < NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD
244                 && mLauncher.getAppsView().getNavBarScrimHeight() > 0;
245         mLauncher.getSystemUiController().updateUiState(
246                 UI_STATE_ALL_APPS, hasScrim ? mNavScrimFlag : 0);
247     }
248 
getProgress()249     public float getProgress() {
250         return mProgress;
251     }
252 
getAppsViewProgressTranslationY()253     private MultiProperty getAppsViewProgressTranslationY() {
254         return mAppsViewTranslationY.get(INDEX_APPS_VIEW_PROGRESS);
255     }
256 
getAppsViewPullbackTranslationY()257     private MultiProperty getAppsViewPullbackTranslationY() {
258         return mAppsViewTranslationY.get(INDEX_APPS_VIEW_PULLBACK);
259     }
260 
getAppsViewProgressAlpha()261     private MultiProperty getAppsViewProgressAlpha() {
262         return mAppsViewAlpha.get(INDEX_APPS_VIEW_PROGRESS);
263     }
264 
getAppsViewPullbackAlpha()265     private MultiProperty getAppsViewPullbackAlpha() {
266         return mAppsViewAlpha.get(INDEX_APPS_VIEW_PULLBACK);
267     }
268 
269     /**
270      * Sets the vertical transition progress to {@param state} and updates all the dependent UI
271      * accordingly.
272      */
273     @Override
setState(LauncherState state)274     public void setState(LauncherState state) {
275         setProgress(state.getVerticalProgress(mLauncher));
276         setAlphas(state, new StateAnimationConfig(), NO_ANIM_PROPERTY_SETTER);
277     }
278 
279     @Override
onBackProgressed( LauncherState toState, @FloatRange(from = 0.0, to = 1.0) float backProgress)280     public void onBackProgressed(
281             LauncherState toState, @FloatRange(from = 0.0, to = 1.0) float backProgress) {
282         if (!mLauncher.isInState(ALL_APPS) || !NORMAL.equals(toState)) {
283             return;
284         }
285 
286         float deceleratedProgress = Interpolators.BACK_GESTURE.getInterpolation(backProgress);
287         float scaleProgress = ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE
288                 + (1 - ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE)
289                 * (1 - deceleratedProgress);
290 
291         mAllAppScale.updateValue(scaleProgress);
292     }
293 
onScaleProgressChanged()294     private void onScaleProgressChanged() {
295         final float scaleProgress = mAllAppScale.value;
296         SCALE_PROPERTY.set(mLauncher.getAppsView(), scaleProgress);
297         if (!mLauncher.getAppsView().isSearching() || !mLauncher.getDeviceProfile().isTablet) {
298             mLauncher.getScrimView().setScrimHeaderScale(scaleProgress);
299         }
300 
301         AllAppsRecyclerView rv = mLauncher.getAppsView().getActiveRecyclerView();
302 
303         // Disable view clipping from all apps' RecyclerView up to all apps view during scale
304         // animation, and vice versa. The goal is to display extra roll(s) app icons (rendered in
305         // {@link AppsGridLayoutManager#calculateExtraLayoutSpace}) during scale animation.
306         boolean hasScaleEffect = scaleProgress < 1f;
307         if (hasScaleEffect != mHasScaleEffect) {
308             mHasScaleEffect = hasScaleEffect;
309             if (mHasScaleEffect) {
310                 modifyAttributesOnViewTree(rv, mLauncher.getAppsView(),
311                         CLIP_CHILDREN_FALSE_MODIFIER);
312             } else {
313                 restoreAttributesOnViewTree(rv, mLauncher.getAppsView(),
314                         CLIP_CHILDREN_FALSE_MODIFIER);
315             }
316         }
317     }
318 
319     /** Set {@link Animator.AnimatorListener} for scaling all apps scale to 1 animation. */
320     public void setAllAppsSearchBackAnimationListener(Animator.AnimatorListener listener) {
321         mAllAppsSearchBackAnimationListener = listener;
322     }
323 
324     /**
325      * Animate all apps view to 1f scale. This is called when backing (exiting) from all apps
326      * search view to all apps view.
327      */
328     public void animateAllAppsToNoScale() {
329         if (mAllAppScale.isAnimating()) {
330             return;
331         }
332         Animator animator = mAllAppScale.animateToValue(1f)
333                 .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS);
334         if (mAllAppsSearchBackAnimationListener != null) {
335             animator.addListener(mAllAppsSearchBackAnimationListener);
336         }
337         animator.start();
338     }
339 
340     /**
341      * Creates an animation which updates the vertical transition progress and updates all the
342      * dependent UI using various animation events
343      *
344      * This method also dictates where along the progress the haptics should be played. As the user
345      * scrolls up from workspace or down from AllApps, a drag haptic is being played until the
346      * commit point where it plays a commit haptic. Where we play the haptics differs when going
347      * from workspace -> allApps and vice versa.
348      */
349     @Override
350     public void setStateWithAnimation(LauncherState toState,
351             StateAnimationConfig config, PendingAnimation builder) {
352         if (mLauncher.isInState(ALL_APPS) && !ALL_APPS.equals(toState)) {
353             builder.addEndListener(success -> {
354                 // Reset pull back progress and alpha after switching states.
355                 ALL_APPS_PULL_BACK_TRANSLATION.set(this, ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT);
356                 ALL_APPS_PULL_BACK_ALPHA.set(this, ALL_APPS_PULL_BACK_ALPHA_DEFAULT);
357 
358                 mAllAppScale.updateValue(1f);
359             });
360         }
361 
362         if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && config.isUserControlled()
363                 && Utilities.ATLEAST_S) {
364             if (toState == ALL_APPS) {
builder.addOnFrameListener( new VibrationAnimatorUpdateListener(this, mVibratorWrapper, SWIPE_DRAG_COMMIT_THRESHOLD, 1))365                 builder.addOnFrameListener(
366                         new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
367                                 SWIPE_DRAG_COMMIT_THRESHOLD, 1));
368             } else {
builder.addOnFrameListener( new VibrationAnimatorUpdateListener(this, mVibratorWrapper, 0, SWIPE_DRAG_COMMIT_THRESHOLD))369                 builder.addOnFrameListener(
370                         new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
371                                 0, SWIPE_DRAG_COMMIT_THRESHOLD));
372             }
builder.addEndListener(unused)373             builder.addEndListener((unused) -> {
374                 mVibratorWrapper.cancelVibrate();
375             });
376         }
377 
378         float targetProgress = toState.getVerticalProgress(mLauncher);
379         if (Float.compare(mProgress, targetProgress) == 0) {
setAlphas(toState, config, builder)380             setAlphas(toState, config, builder);
381             // Fail fast
382             return;
383         }
384 
385         // need to decide depending on the release velocity
386         Interpolator verticalProgressInterpolator = config.getInterpolator(ANIM_VERTICAL_PROGRESS,
387                 config.isUserControlled() ? LINEAR : DECELERATE_1_7);
388         Animator anim = createSpringAnimation(mProgress, targetProgress);
anim.setInterpolator(verticalProgressInterpolator)389         anim.setInterpolator(verticalProgressInterpolator);
builder.add(anim)390         builder.add(anim);
391 
setAlphas(toState, config, builder)392         setAlphas(toState, config, builder);
393         // This controls both haptics for tapping on QSB and going to all apps.
394         if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL) &&
395                 !FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get()) {
396             mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
397                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
398         }
399     }
400 
createSpringAnimation(float... progressValues)401     public Animator createSpringAnimation(float... progressValues) {
402         return ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, progressValues);
403     }
404 
405     /**
406      * Updates the property for the provided state
407      */
setAlphas(LauncherState state, StateAnimationConfig config, PropertySetter setter)408     public void setAlphas(LauncherState state, StateAnimationConfig config, PropertySetter setter) {
409         int visibleElements = state.getVisibleElements(mLauncher);
410         boolean hasAllAppsContent = (visibleElements & ALL_APPS_CONTENT) != 0;
411 
412         Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
413         setter.setFloat(getAppsViewProgressAlpha(), MultiPropertyFactory.MULTI_PROPERTY_VALUE,
414                 hasAllAppsContent ? 1 : 0, allAppsFade);
415         setter.setFloat(getAppsViewPullbackAlpha(), MultiPropertyFactory.MULTI_PROPERTY_VALUE,
416                 hasAllAppsContent ? 1 : 0, allAppsFade);
417 
418         setter.setFloat(mLauncher.getAppsView(),
419                 ActivityAllAppsContainerView.BOTTOM_SHEET_ALPHA, hasAllAppsContent ? 1 : 0,
420                 config.getInterpolator(ANIM_ALL_APPS_BOTTOM_SHEET_FADE, INSTANT));
421 
422         boolean shouldProtectHeader = !config.hasAnimationFlag(StateAnimationConfig.SKIP_SCRIM)
423                 && (ALL_APPS == state || mLauncher.getStateManager().getState() == ALL_APPS);
424         mScrimView.setDrawingController(shouldProtectHeader ? mAppsView : null);
425     }
426 
427     /**
428      * see Launcher#setupViews
429      */
setupViews(ScrimView scrimView, ActivityAllAppsContainerView<Launcher> appsView)430     public void setupViews(ScrimView scrimView, ActivityAllAppsContainerView<Launcher> appsView) {
431         mScrimView = scrimView;
432         mAppsView = appsView;
433         mAppsView.setScrimView(scrimView);
434 
435         mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT,
436                 FeatureFlags.ALL_APPS_GONE_VISIBILITY.get() ? View.GONE : View.INVISIBLE);
437         mAppsViewAlpha.setUpdateVisibility(true);
438         mAppsViewTranslationY = new MultiPropertyFactory<>(
439                 mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum);
440     }
441 
442     /**
443      * Updates the total scroll range but does not update the UI.
444      */
setShiftRange(float shiftRange)445     public void setShiftRange(float shiftRange) {
446         mShiftRange = shiftRange;
447     }
448 
449     /**
450      * This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start
451      * threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller
452      * as it gives an accurate progress that dictates where the vibrator should vibrate.
453      * Note: once the user begins a gesture and does the commit haptic, there should not be anymore
454      * haptics played for that gesture.
455      */
456     private static class VibrationAnimatorUpdateListener implements
457             ValueAnimator.AnimatorUpdateListener {
458         private final VibratorWrapper mVibratorWrapper;
459         private final AllAppsTransitionController mController;
460         private final float mStartThreshold;
461         private final float mEndThreshold;
462         private boolean mHasCommitted;
463 
VibrationAnimatorUpdateListener(AllAppsTransitionController controller, VibratorWrapper vibratorWrapper, float startThreshold, float endThreshold)464         VibrationAnimatorUpdateListener(AllAppsTransitionController controller,
465                                         VibratorWrapper vibratorWrapper, float startThreshold,
466                                         float endThreshold) {
467             mController = controller;
468             mVibratorWrapper = vibratorWrapper;
469             mStartThreshold = startThreshold;
470             mEndThreshold = endThreshold;
471         }
472 
473         @Override
onAnimationUpdate(ValueAnimator animation)474         public void onAnimationUpdate(ValueAnimator animation) {
475             if (mHasCommitted) {
476                 return;
477             }
478             float currentProgress =
479                     AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController);
480             if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) {
481                 mVibratorWrapper.vibrateForDragTexture();
482             } else if (!(currentProgress == 0 || currentProgress == 1)) {
483                 // This check guards against committing at the location of the start of the gesture
484                 mVibratorWrapper.vibrateForDragCommit();
485                 mHasCommitted = true;
486             }
487         }
488     }
489 }
490