1 package com.android.launcher3.allapps;
2 
3 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
4 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
5 import static com.android.launcher3.LauncherState.OVERVIEW;
6 import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
7 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
8 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_HEADER_FADE;
9 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
10 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
11 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
12 import static com.android.launcher3.anim.Interpolators.LINEAR;
13 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
14 import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
15 
16 import android.animation.Animator;
17 import android.animation.AnimatorListenerAdapter;
18 import android.util.FloatProperty;
19 import android.view.animation.Interpolator;
20 
21 import com.android.launcher3.DeviceProfile;
22 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
23 import com.android.launcher3.Launcher;
24 import com.android.launcher3.LauncherState;
25 import com.android.launcher3.LauncherStateManager.AnimationConfig;
26 import com.android.launcher3.LauncherStateManager.StateHandler;
27 import com.android.launcher3.R;
28 import com.android.launcher3.anim.AnimationSuccessListener;
29 import com.android.launcher3.anim.AnimatorSetBuilder;
30 import com.android.launcher3.anim.PropertySetter;
31 import com.android.launcher3.anim.SpringObjectAnimator;
32 import com.android.launcher3.util.Themes;
33 import com.android.launcher3.views.ScrimView;
34 
35 /**
36  * Handles AllApps view transition.
37  * 1) Slides all apps view using direct manipulation
38  * 2) When finger is released, animate to either top or bottom accordingly.
39  * <p/>
40  * Algorithm:
41  * If release velocity > THRES1, snap according to the direction of movement.
42  * If release velocity < THRES1, snap according to either top or bottom depending on whether it's
43  * closer to top or closer to the page indicator.
44  */
45 public class AllAppsTransitionController implements StateHandler, OnDeviceProfileChangeListener {
46 
47     private static final float SPRING_DAMPING_RATIO = 0.9f;
48     private static final float SPRING_STIFFNESS = 600f;
49 
50     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
51             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
52 
53         @Override
54         public Float get(AllAppsTransitionController controller) {
55             return controller.mProgress;
56         }
57 
58         @Override
59         public void setValue(AllAppsTransitionController controller, float progress) {
60             controller.setProgress(progress);
61         }
62     };
63 
64     private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 0;
65 
66     private AllAppsContainerView mAppsView;
67     private ScrimView mScrimView;
68 
69     private final Launcher mLauncher;
70     private final boolean mIsDarkTheme;
71     private boolean mIsVerticalLayout;
72 
73     // Animation in this class is controlled by a single variable {@link mProgress}.
74     // Visually, it represents top y coordinate of the all apps container if multiplied with
75     // {@link mShiftRange}.
76 
77     // When {@link mProgress} is 0, all apps container is pulled up.
78     // When {@link mProgress} is 1, all apps container is pulled down.
79     private float mShiftRange;      // changes depending on the orientation
80     private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent
81 
82     private float mScrollRangeDelta = 0;
83 
AllAppsTransitionController(Launcher l)84     public AllAppsTransitionController(Launcher l) {
85         mLauncher = l;
86         mShiftRange = mLauncher.getDeviceProfile().heightPx;
87         mProgress = 1f;
88 
89         mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark);
90         mIsVerticalLayout = mLauncher.getDeviceProfile().isVerticalBarLayout();
91         mLauncher.addOnDeviceProfileChangeListener(this);
92     }
93 
getShiftRange()94     public float getShiftRange() {
95         return mShiftRange;
96     }
97 
98     @Override
onDeviceProfileChanged(DeviceProfile dp)99     public void onDeviceProfileChanged(DeviceProfile dp) {
100         mIsVerticalLayout = dp.isVerticalBarLayout();
101         setScrollRangeDelta(mScrollRangeDelta);
102 
103         if (mIsVerticalLayout) {
104             mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1);
105             mLauncher.getHotseat().setTranslationY(0);
106             mLauncher.getWorkspace().getPageIndicator().setTranslationY(0);
107         }
108     }
109 
110     /**
111      * Note this method should not be called outside this class. This is public because it is used
112      * in xml-based animations which also handle updating the appropriate UI.
113      *
114      * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace
115      *
116      * @see #setState(LauncherState)
117      * @see #setStateWithAnimation(LauncherState, AnimatorSetBuilder, AnimationConfig)
118      */
setProgress(float progress)119     public void setProgress(float progress) {
120         mProgress = progress;
121         mScrimView.setProgress(progress);
122         float shiftCurrent = progress * mShiftRange;
123 
124         mAppsView.setTranslationY(shiftCurrent);
125 
126         // Use a light system UI (dark icons) if all apps is behind at least half of the
127         // status bar.
128         boolean forceChange = shiftCurrent - mScrimView.getDragHandleSize()
129                 <= mLauncher.getDeviceProfile().getInsets().top / 2;
130         if (forceChange) {
131             mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, !mIsDarkTheme);
132         } else {
133             mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, 0);
134         }
135     }
136 
getProgress()137     public float getProgress() {
138         return mProgress;
139     }
140 
141     /**
142      * Sets the vertical transition progress to {@param state} and updates all the dependent UI
143      * accordingly.
144      */
145     @Override
setState(LauncherState state)146     public void setState(LauncherState state) {
147         setProgress(state.getVerticalProgress(mLauncher));
148         setAlphas(state, null, new AnimatorSetBuilder());
149         onProgressAnimationEnd();
150     }
151 
152     /**
153      * Creates an animation which updates the vertical transition progress and updates all the
154      * dependent UI using various animation events
155      */
156     @Override
setStateWithAnimation(LauncherState toState, AnimatorSetBuilder builder, AnimationConfig config)157     public void setStateWithAnimation(LauncherState toState,
158             AnimatorSetBuilder builder, AnimationConfig config) {
159         float targetProgress = toState.getVerticalProgress(mLauncher);
160         if (Float.compare(mProgress, targetProgress) == 0) {
161             setAlphas(toState, config, builder);
162             // Fail fast
163             onProgressAnimationEnd();
164             return;
165         }
166 
167         if (!config.playNonAtomicComponent()) {
168             // There is no atomic component for the all apps transition, so just return early.
169             return;
170         }
171 
172         Interpolator interpolator = config.userControlled ? LINEAR : toState == OVERVIEW
173                 ? builder.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN)
174                 : FAST_OUT_SLOW_IN;
175         Animator anim = createSpringAnimation(mProgress, targetProgress);
176         anim.setDuration(config.duration);
177         anim.setInterpolator(builder.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator));
178         anim.addListener(getProgressAnimatorListener());
179 
180         builder.play(anim);
181 
182         setAlphas(toState, config, builder);
183     }
184 
createSpringAnimation(float... progressValues)185     public Animator createSpringAnimation(float... progressValues) {
186         return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
187                 SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
188     }
189 
setAlphas(LauncherState toState, AnimationConfig config, AnimatorSetBuilder builder)190     private void setAlphas(LauncherState toState, AnimationConfig config,
191             AnimatorSetBuilder builder) {
192         setAlphas(toState.getVisibleElements(mLauncher), config, builder);
193     }
194 
setAlphas(int visibleElements, AnimationConfig config, AnimatorSetBuilder builder)195     public void setAlphas(int visibleElements, AnimationConfig config, AnimatorSetBuilder builder) {
196         PropertySetter setter = config == null ? NO_ANIM_PROPERTY_SETTER
197                 : config.getPropertySetter(builder);
198         boolean hasHeaderExtra = (visibleElements & ALL_APPS_HEADER_EXTRA) != 0;
199         boolean hasAllAppsContent = (visibleElements & ALL_APPS_CONTENT) != 0;
200 
201         Interpolator allAppsFade = builder.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
202         Interpolator headerFade = builder.getInterpolator(ANIM_ALL_APPS_HEADER_FADE, allAppsFade);
203         setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
204         setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
205         mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra, hasAllAppsContent,
206                 setter, headerFade, allAppsFade);
207         mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);
208 
209         setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA,
210                 (visibleElements & VERTICAL_SWIPE_INDICATOR) != 0 ? 255 : 0, allAppsFade);
211     }
212 
getProgressAnimatorListener()213     public AnimatorListenerAdapter getProgressAnimatorListener() {
214         return new AnimationSuccessListener() {
215             @Override
216             public void onAnimationSuccess(Animator animator) {
217                 onProgressAnimationEnd();
218             }
219         };
220     }
221 
222     public void setupViews(AllAppsContainerView appsView) {
223         mAppsView = appsView;
224         mScrimView = mLauncher.findViewById(R.id.scrim_view);
225     }
226 
227     /**
228      * Updates the total scroll range but does not update the UI.
229      */
230     void setScrollRangeDelta(float delta) {
231         mScrollRangeDelta = delta;
232         mShiftRange = mLauncher.getDeviceProfile().heightPx - mScrollRangeDelta;
233 
234         if (mScrimView != null) {
235             mScrimView.reInitUi();
236         }
237     }
238 
239     /**
240      * Set the final view states based on the progress.
241      * TODO: This logic should go in {@link LauncherState}
242      */
243     private void onProgressAnimationEnd() {
244         if (Float.compare(mProgress, 1f) == 0) {
245             mAppsView.reset(false /* animate */);
246         } else if (isAllAppsExpanded()) {
247             mAppsView.onScrollUpEnd();
248         }
249     }
250 
251     private boolean isAllAppsExpanded() {
252         return Float.compare(mProgress, 0f) == 0;
253     }
254 
255     public void highlightWorkTabIfNecessary() {
256         if (isAllAppsExpanded()) {
257             mAppsView.highlightWorkTabIfNecessary();
258         }
259     }
260 }
261