1 /*
2  * Copyright (C) 2018 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.uioverrides.touchcontrollers;
17 
18 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
19 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
20 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
21 import static com.android.launcher3.LauncherState.ALL_APPS;
22 import static com.android.launcher3.LauncherState.NORMAL;
23 import static com.android.launcher3.LauncherState.OVERVIEW;
24 import static com.android.launcher3.anim.Interpolators.ACCEL;
25 import static com.android.launcher3.anim.Interpolators.DEACCEL;
26 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
27 import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
28 
29 import android.view.MotionEvent;
30 
31 import com.android.launcher3.DeviceProfile;
32 import com.android.launcher3.Launcher;
33 import com.android.launcher3.LauncherState;
34 import com.android.launcher3.allapps.AllAppsTransitionController;
35 import com.android.launcher3.anim.Interpolators;
36 import com.android.launcher3.states.StateAnimationConfig;
37 import com.android.launcher3.touch.AbstractStateChangeTouchController;
38 import com.android.launcher3.touch.SingleAxisSwipeDetector;
39 import com.android.launcher3.uioverrides.states.OverviewState;
40 import com.android.quickstep.SystemUiProxy;
41 import com.android.quickstep.util.LayoutUtils;
42 import com.android.quickstep.views.RecentsView;
43 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
44 
45 /**
46  * Touch controller for handling various state transitions in portrait UI.
47  */
48 public class PortraitStatesTouchController extends AbstractStateChangeTouchController {
49 
50     private static final String TAG = "PortraitStatesTouchCtrl";
51 
52     /**
53      * The progress at which all apps content will be fully visible.
54      */
55     public static final float ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD = 0.8f;
56 
57     /**
58      * Minimum clamping progress for fading in all apps content
59      */
60     public static final float ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD = 0.5f;
61 
62     /**
63      * Minimum clamping progress for fading in all apps scrim
64      */
65     public static final float ALL_APPS_SCRIM_VISIBLE_THRESHOLD = .1f;
66 
67     /**
68      * Maximum clamping progress for opaque all apps scrim
69      */
70     public static final float ALL_APPS_SCRIM_OPAQUE_THRESHOLD = .5f;
71 
72     private final PortraitOverviewStateTouchHelper mOverviewPortraitStateTouchHelper;
73 
PortraitStatesTouchController(Launcher l)74     public PortraitStatesTouchController(Launcher l) {
75         super(l, SingleAxisSwipeDetector.VERTICAL);
76         mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l);
77     }
78 
79     @Override
canInterceptTouch(MotionEvent ev)80     protected boolean canInterceptTouch(MotionEvent ev) {
81         // If we are swiping to all apps instead of overview, allow it from anywhere.
82         boolean interceptAnywhere = mLauncher.isInState(NORMAL);
83         if (mCurrentAnimation != null) {
84             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
85             if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()
86                     || interceptAnywhere) {
87                 // If we are already animating from a previous state, we can intercept as long as
88                 // the touch is below the current all apps progress (to allow for double swipe).
89                 return true;
90             }
91             // Otherwise, don't intercept so they can scroll recents, dismiss a task, etc.
92             return false;
93         }
94         if (mLauncher.isInState(ALL_APPS)) {
95             // In all-apps only listen if the container cannot scroll itself
96             if (!mLauncher.getAppsView().shouldContainerScroll(ev)) {
97                 return false;
98             }
99         } else if (mLauncher.isInState(OVERVIEW)) {
100             if (!mOverviewPortraitStateTouchHelper.canInterceptTouch(ev)) {
101                 return false;
102             }
103         } else {
104             // For non-normal states, only listen if the event originated below the hotseat height
105             if (!interceptAnywhere && !isTouchOverHotseat(mLauncher, ev)) {
106                 return false;
107             }
108         }
109         if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
110             return false;
111         }
112         return true;
113     }
114 
115     @Override
getTargetState(LauncherState fromState, boolean isDragTowardPositive)116     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
117         if (fromState == ALL_APPS && !isDragTowardPositive) {
118             return NORMAL;
119         } else if (fromState == OVERVIEW) {
120             return isDragTowardPositive ? OVERVIEW : NORMAL;
121         } else if (fromState == NORMAL && isDragTowardPositive) {
122             return ALL_APPS;
123         }
124         return fromState;
125     }
126 
getNormalToAllAppsAnimation()127     private StateAnimationConfig getNormalToAllAppsAnimation() {
128         StateAnimationConfig builder = new StateAnimationConfig();
129         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
130                 ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD,
131                 ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD));
132         builder.setInterpolator(ANIM_SCRIM_FADE, Interpolators.clampToProgress(ACCEL,
133                 ALL_APPS_SCRIM_VISIBLE_THRESHOLD,
134                 ALL_APPS_SCRIM_OPAQUE_THRESHOLD));
135         return builder;
136     }
137 
getAllAppsToNormalAnimation()138     private StateAnimationConfig getAllAppsToNormalAnimation() {
139         StateAnimationConfig builder = new StateAnimationConfig();
140         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL,
141                 1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD,
142                 1 - ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD));
143         builder.setInterpolator(ANIM_SCRIM_FADE, Interpolators.clampToProgress(DEACCEL,
144                 1 - ALL_APPS_SCRIM_OPAQUE_THRESHOLD,
145                 1 - ALL_APPS_SCRIM_VISIBLE_THRESHOLD));
146         return builder;
147     }
148 
149     @Override
getConfigForStates( LauncherState fromState, LauncherState toState)150     protected StateAnimationConfig getConfigForStates(
151             LauncherState fromState, LauncherState toState) {
152         final StateAnimationConfig config;
153         if (fromState == NORMAL && toState == ALL_APPS) {
154             config = getNormalToAllAppsAnimation();
155         } else if (fromState == ALL_APPS && toState == NORMAL) {
156             config = getAllAppsToNormalAnimation();
157         } else {
158             config = new StateAnimationConfig();
159         }
160         return config;
161     }
162 
163     @Override
initCurrentAnimation()164     protected float initCurrentAnimation() {
165         float range = getShiftRange();
166         long maxAccuracy = (long) (2 * range);
167 
168         float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
169         float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
170 
171         float totalShift = endVerticalShift - startVerticalShift;
172 
173         final StateAnimationConfig config = totalShift == 0 ? new StateAnimationConfig()
174                 : getConfigForStates(mFromState, mToState);
175         config.duration = maxAccuracy;
176 
177         if (mCurrentAnimation != null) {
178             mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
179             mCurrentAnimation.dispatchOnCancel();
180         }
181 
182         mGoingBetweenStates = true;
183         if (mFromState == OVERVIEW && mToState == NORMAL
184                 && mOverviewPortraitStateTouchHelper.shouldSwipeDownReturnToApp()) {
185             // Reset the state manager, when changing the interaction mode
186             mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */);
187             mGoingBetweenStates = false;
188             mCurrentAnimation = mOverviewPortraitStateTouchHelper
189                     .createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR)
190                     .createPlaybackController();
191             mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
192             RecentsView recentsView = mLauncher.getOverviewPanel();
193             totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher,
194                     mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler());
195         } else {
196             mCurrentAnimation = mLauncher.getStateManager()
197                     .createAnimationToNewWorkspace(mToState, config);
198         }
199         mCurrentAnimation.getTarget().addListener(mClearStateOnCancelListener);
200 
201         if (totalShift == 0) {
202             totalShift = Math.signum(mFromState.ordinal - mToState.ordinal)
203                     * OverviewState.getDefaultSwipeHeight(mLauncher);
204         }
205         return 1 / totalShift;
206     }
207 
208     @Override
onSwipeInteractionCompleted(LauncherState targetState)209     protected void onSwipeInteractionCompleted(LauncherState targetState) {
210         super.onSwipeInteractionCompleted(targetState);
211         if (mStartState == NORMAL && targetState == OVERVIEW) {
212             SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
213         }
214     }
215 
216     /**
217      * Whether the motion event is over the hotseat.
218      *
219      * @param launcher the launcher activity
220      * @param ev       the event to check
221      * @return true if the event is over the hotseat
222      */
isTouchOverHotseat(Launcher launcher, MotionEvent ev)223     static boolean isTouchOverHotseat(Launcher launcher, MotionEvent ev) {
224         return (ev.getY() >= getHotseatTop(launcher));
225     }
226 
getHotseatTop(Launcher launcher)227     public static int getHotseatTop(Launcher launcher) {
228         DeviceProfile dp = launcher.getDeviceProfile();
229         int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
230         return launcher.getDragLayer().getHeight() - hotseatHeight;
231     }
232 
233     @Override
onControllerInterceptTouchEvent(MotionEvent ev)234     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
235         switch (ev.getAction()) {
236             case MotionEvent.ACTION_DOWN:
237                 InteractionJankMonitorWrapper.begin(
238                         mLauncher.getRootView(), InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
239                 break;
240 
241             case MotionEvent.ACTION_CANCEL:
242             case MotionEvent.ACTION_UP:
243                 InteractionJankMonitorWrapper.cancel(
244                         InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
245                 break;
246         }
247         return super.onControllerInterceptTouchEvent(ev);
248 
249     }
250 
251     @Override
onReinitToState(LauncherState newToState)252     protected void onReinitToState(LauncherState newToState) {
253         super.onReinitToState(newToState);
254         if (newToState != ALL_APPS) {
255             InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
256         }
257     }
258 
259     @Override
onReachedFinalState(LauncherState toState)260     protected void onReachedFinalState(LauncherState toState) {
261         super.onReinitToState(toState);
262         if (toState == ALL_APPS) {
263             InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
264         }
265     }
266 
267     @Override
clearState()268     protected void clearState() {
269         super.clearState();
270         InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
271     }
272 }
273