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