• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.uioverrides.touchcontrollers;
18 
19 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
20 import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
21 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
22 import static com.android.launcher3.LauncherState.ALL_APPS;
23 import static com.android.launcher3.LauncherState.HINT_STATE;
24 import static com.android.launcher3.LauncherState.NORMAL;
25 import static com.android.launcher3.LauncherState.OVERVIEW;
26 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
27 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
28 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
29 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
30 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
31 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
32 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
33 
34 import android.animation.AnimatorSet;
35 import android.animation.ObjectAnimator;
36 import android.animation.ValueAnimator;
37 import android.graphics.PointF;
38 import android.view.MotionEvent;
39 import android.view.ViewConfiguration;
40 
41 import com.android.launcher3.Launcher;
42 import com.android.launcher3.LauncherState;
43 import com.android.launcher3.Utilities;
44 import com.android.launcher3.anim.AnimatorPlaybackController;
45 import com.android.launcher3.config.FeatureFlags;
46 import com.android.launcher3.states.StateAnimationConfig;
47 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
48 import com.android.launcher3.uioverrides.QuickstepLauncher;
49 import com.android.launcher3.util.DisplayController;
50 import com.android.launcher3.util.VibratorWrapper;
51 import com.android.quickstep.SystemUiProxy;
52 import com.android.quickstep.util.AnimatorControllerWithResistance;
53 import com.android.quickstep.util.MotionPauseDetector;
54 import com.android.quickstep.util.OverviewToHomeAnim;
55 import com.android.quickstep.views.RecentsView;
56 
57 import java.util.function.Consumer;
58 
59 /**
60  * Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above
61  * the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the
62  * first home screen instead of to Overview.
63  */
64 public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouchController {
65     private static final float ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER = 2.5f;
66 
67     // How much of the movement to use for translating overview after swipe and hold.
68     private static final float OVERVIEW_MOVEMENT_FACTOR = 0.25f;
69     private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
70     private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
71 
72     private final VibratorWrapper mVibratorWrapper;
73     private final Consumer<AnimatorSet> mCancelSplitRunnable;
74     private final RecentsView mRecentsView;
75     private final MotionPauseDetector mMotionPauseDetector;
76     private final float mMotionPauseMinDisplacement;
77 
78     private boolean mDidTouchStartInNavBar;
79     private boolean mStartedOverview;
80     private boolean mReachedOverview;
81     // The last recorded displacement before we reached overview.
82     private PointF mStartDisplacement = new PointF();
83     private float mStartY;
84     private AnimatorPlaybackController mOverviewResistYAnim;
85 
86     // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
87     private ObjectAnimator mNormalToHintOverviewScrimAnimator;
88 
89     /**
90      * @param cancelSplitRunnable Called when split placeholder view needs to be cancelled.
91      *                            Animation should be added to the provided AnimatorSet
92      */
NoButtonNavbarToOverviewTouchController(Launcher l, Consumer<AnimatorSet> cancelSplitRunnable)93     public NoButtonNavbarToOverviewTouchController(Launcher l,
94             Consumer<AnimatorSet> cancelSplitRunnable) {
95         super(l);
96         mRecentsView = l.getOverviewPanel();
97         mMotionPauseDetector = new MotionPauseDetector(l);
98         mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
99         mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext());
100         mCancelSplitRunnable = cancelSplitRunnable;
101     }
102 
103     @Override
canInterceptTouch(MotionEvent ev)104     protected boolean canInterceptTouch(MotionEvent ev) {
105         if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher)
106                 == THREE_BUTTONS) {
107             return false;
108         }
109         mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
110         boolean isOneHandedModeActive = (SystemUiProxy.INSTANCE.get(mLauncher)
111                 .getLastSystemUiStateFlags() & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0;
112         // Reset touch slop multiplier to default 1.0f if one-handed-mode is not active
113         mDetector.setTouchSlopMultiplier(
114                 isOneHandedModeActive ? ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER : 1f /* default */);
115         return super.canInterceptTouch(ev) && !mLauncher.isInState(HINT_STATE);
116     }
117 
118     @Override
getTargetState(LauncherState fromState, boolean isDragTowardPositive)119     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
120         if (fromState == NORMAL && mDidTouchStartInNavBar) {
121             return HINT_STATE;
122         }
123         return super.getTargetState(fromState, isDragTowardPositive);
124     }
125 
126     @Override
initCurrentAnimation()127     protected float initCurrentAnimation() {
128         float progressMultiplier = super.initCurrentAnimation();
129         if (mToState == HINT_STATE) {
130             // Track the drag across the entire height of the screen.
131             progressMultiplier = -1f / mLauncher.getDeviceProfile().heightPx;
132         }
133         return progressMultiplier;
134     }
135 
136     @Override
onDragStart(boolean start, float startDisplacement)137     public void onDragStart(boolean start, float startDisplacement) {
138         if (mLauncher.isInState(ALL_APPS)) {
139             LauncherTaskbarUIController controller =
140                     ((QuickstepLauncher) mLauncher).getTaskbarUIController();
141             if (controller != null) {
142                 controller.setShouldDelayLauncherStateAnim(true);
143             }
144         }
145 
146         super.onDragStart(start, startDisplacement);
147 
148         mMotionPauseDetector.clear();
149 
150         if (handlingOverviewAnim()) {
151             mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
152         }
153 
154         if (mFromState == NORMAL && mToState == HINT_STATE) {
155             mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofArgb(
156                     mLauncher.getScrimView(),
157                     VIEW_BACKGROUND_COLOR,
158                     mFromState.getWorkspaceScrimColor(mLauncher),
159                     mToState.getWorkspaceScrimColor(mLauncher));
160         }
161         mStartedOverview = false;
162         mReachedOverview = false;
163         mOverviewResistYAnim = null;
164     }
165 
166     @Override
updateProgress(float fraction)167     protected void updateProgress(float fraction) {
168         super.updateProgress(fraction);
169         if (mNormalToHintOverviewScrimAnimator != null) {
170             mNormalToHintOverviewScrimAnimator.setCurrentFraction(fraction);
171         }
172     }
173 
174     @Override
onDragEnd(float velocity)175     public void onDragEnd(float velocity) {
176         LauncherTaskbarUIController controller =
177                 ((QuickstepLauncher) mLauncher).getTaskbarUIController();
178         if (controller != null) {
179             controller.setShouldDelayLauncherStateAnim(false);
180         }
181 
182         if (mStartedOverview) {
183             goToOverviewOrHomeOnDragEnd(velocity);
184         } else {
185             super.onDragEnd(velocity);
186         }
187 
188         mMotionPauseDetector.clear();
189         mNormalToHintOverviewScrimAnimator = null;
190         if (mLauncher.isInState(OVERVIEW)) {
191             // Normally we would cleanup the state based on mCurrentAnimation, but since we stop
192             // using that when we pause to go to Overview, we need to clean up ourselves.
193             clearState();
194         }
195     }
196 
197     @Override
updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling)198     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
199             LauncherState targetState, float velocity, boolean isFling) {
200         super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity,
201                 isFling);
202         if (targetState == HINT_STATE) {
203             // Normally we compute the duration based on the velocity and distance to the given
204             // state, but since the hint state tracks the entire screen without a clear endpoint, we
205             // need to manually set the duration to a reasonable value.
206             animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */));
207             AnimatorSet animatorSet = new AnimatorSet();
208             mCancelSplitRunnable.accept(animatorSet);
209             animatorSet.start();
210         }
211         if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() &&
212                 ((mFromState == NORMAL && mToState == ALL_APPS)
213                         || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) {
214             mVibratorWrapper.vibrateForDragBump();
215         }
216     }
217 
onMotionPauseDetected()218     private void onMotionPauseDetected() {
219         if (mCurrentAnimation == null) {
220             return;
221         }
222         mNormalToHintOverviewScrimAnimator = null;
223         mCurrentAnimation.getTarget().addListener(newCancelListener(() ->
224                 mLauncher.getStateManager().goToState(OVERVIEW, true, forSuccessCallback(() -> {
225                     mOverviewResistYAnim = AnimatorControllerWithResistance
226                             .createRecentsResistanceFromOverviewAnim(mLauncher, null)
227                             .createPlaybackController();
228                     mReachedOverview = true;
229                     maybeSwipeInteractionToOverviewComplete();
230                 }))));
231 
232         mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
233         mCurrentAnimation.dispatchOnCancel();
234         mStartedOverview = true;
235         VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
236     }
237 
maybeSwipeInteractionToOverviewComplete()238     private void maybeSwipeInteractionToOverviewComplete() {
239         if (mReachedOverview && !mDetector.isDraggingState()) {
240             onSwipeInteractionCompleted(OVERVIEW);
241         }
242     }
243 
handlingOverviewAnim()244     private boolean handlingOverviewAnim() {
245         int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
246         return mDidTouchStartInNavBar && mStartState == NORMAL
247                 && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
248     }
249 
250     @Override
onDrag(float yDisplacement, float xDisplacement, MotionEvent event)251     public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
252         if (mStartedOverview) {
253             if (!mReachedOverview) {
254                 mStartDisplacement.set(xDisplacement, yDisplacement);
255                 mStartY = event.getY();
256             } else {
257                 mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x)
258                         * OVERVIEW_MOVEMENT_FACTOR);
259                 float yProgress = (mStartDisplacement.y - yDisplacement) / mStartY;
260                 if (yProgress > 0 && mOverviewResistYAnim != null) {
261                     mOverviewResistYAnim.setPlayFraction(yProgress);
262                 } else {
263                     mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
264                             * OVERVIEW_MOVEMENT_FACTOR);
265                 }
266             }
267         }
268 
269         float upDisplacement = -yDisplacement;
270         mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim()
271                 || upDisplacement < mMotionPauseMinDisplacement);
272         mMotionPauseDetector.addPosition(event);
273 
274         // Stay in Overview.
275         return mStartedOverview || super.onDrag(yDisplacement, xDisplacement, event);
276     }
277 
goToOverviewOrHomeOnDragEnd(float velocity)278     private void goToOverviewOrHomeOnDragEnd(float velocity) {
279         boolean goToHomeInsteadOfOverview = !mMotionPauseDetector.isPaused();
280         if (goToHomeInsteadOfOverview) {
281             new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(NORMAL), null)
282                     .animateWithVelocity(velocity);
283         }
284         if (mReachedOverview) {
285             float distanceDp = dpiFromPx(Math.max(
286                     Math.abs(mRecentsView.getTranslationX()),
287                     Math.abs(mRecentsView.getTranslationY())));
288             long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS,
289                     distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS);
290             mRecentsView.animate()
291                     .translationX(0)
292                     .translationY(0)
293                     .setInterpolator(ACCELERATE_DECELERATE)
294                     .setDuration(duration)
295                     .withEndAction(goToHomeInsteadOfOverview
296                             ? null
297                             : this::maybeSwipeInteractionToOverviewComplete);
298             if (!goToHomeInsteadOfOverview) {
299                 // Return to normal properties for the overview state.
300                 StateAnimationConfig config = new StateAnimationConfig();
301                 config.duration = duration;
302                 LauncherState state = mLauncher.getStateManager().getState();
303                 mLauncher.getStateManager().createAtomicAnimation(state, state, config).start();
304             }
305         }
306     }
307 
dpiFromPx(float pixels)308     private float dpiFromPx(float pixels) {
309         return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics().densityDpi);
310     }
311 }
312