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