• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_TOUCH_CONTROLLER_NO_INTERCEPT;
19 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
20 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.content.Context;
25 import android.os.VibrationEffect;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.animation.Interpolator;
29 
30 import com.android.app.animation.Interpolators;
31 import com.android.launcher3.AbstractFloatingView;
32 import com.android.launcher3.LauncherAnimUtils;
33 import com.android.launcher3.R;
34 import com.android.launcher3.Utilities;
35 import com.android.launcher3.anim.AnimatorPlaybackController;
36 import com.android.launcher3.anim.PendingAnimation;
37 import com.android.launcher3.touch.BaseSwipeDetector;
38 import com.android.launcher3.touch.SingleAxisSwipeDetector;
39 import com.android.launcher3.util.DisplayController;
40 import com.android.launcher3.util.FlingBlockCheck;
41 import com.android.launcher3.util.TouchController;
42 import com.android.launcher3.util.VibratorWrapper;
43 import com.android.launcher3.views.BaseDragLayer;
44 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
45 import com.android.quickstep.util.VibrationConstants;
46 import com.android.quickstep.views.RecentsView;
47 import com.android.quickstep.views.RecentsViewContainer;
48 import com.android.quickstep.views.TaskView;
49 
50 /**
51  * Touch controller for handling task view card swipes
52  */
53 public abstract class TaskViewTouchController<CONTAINER extends Context & RecentsViewContainer>
54         extends AnimatorListenerAdapter implements TouchController,
55         SingleAxisSwipeDetector.Listener {
56 
57     private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f;
58     private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300;
59     private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600;
60 
61     public static final int TASK_DISMISS_VIBRATION_PRIMITIVE =
62             VibrationEffect.Composition.PRIMITIVE_TICK;
63     public static final float TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE = 1f;
64     public static final VibrationEffect TASK_DISMISS_VIBRATION_FALLBACK =
65             VibrationConstants.EFFECT_TEXTURE_TICK;
66 
67     protected final CONTAINER mContainer;
68     private final SingleAxisSwipeDetector mDetector;
69     private final RecentsView mRecentsView;
70     private final int[] mTempCords = new int[2];
71     private final boolean mIsRtl;
72 
73     private AnimatorPlaybackController mCurrentAnimation;
74     private boolean mCurrentAnimationIsGoingUp;
75     private boolean mAllowGoingUp;
76     private boolean mAllowGoingDown;
77 
78     private boolean mNoIntercept;
79 
80     private float mDisplacementShift;
81     private float mProgressMultiplier;
82     private float mEndDisplacement;
83     private boolean mDraggingEnabled = true;
84     private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
85     private Float mOverrideVelocity = null;
86 
87     private TaskView mTaskBeingDragged;
88 
89     private boolean mIsDismissHapticRunning = false;
90 
TaskViewTouchController(CONTAINER container)91     public TaskViewTouchController(CONTAINER container) {
92         mContainer = container;
93         mRecentsView = container.getOverviewPanel();
94         mIsRtl = Utilities.isRtl(container.getResources());
95         SingleAxisSwipeDetector.Direction dir =
96                 mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection();
97         mDetector = new SingleAxisSwipeDetector(container, this, dir);
98     }
99 
canInterceptTouch(MotionEvent ev)100     private boolean canInterceptTouch(MotionEvent ev) {
101         if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0) {
102             // Don't intercept swipes on the nav bar, as user might be trying to go home
103             // during a task dismiss animation.
104             if (mCurrentAnimation != null) {
105                 mCurrentAnimation.getAnimationPlayer().end();
106             }
107             return false;
108         }
109         if (mCurrentAnimation != null) {
110             mCurrentAnimation.forceFinishIfCloseToEnd();
111         }
112         if (mCurrentAnimation != null) {
113             // If we are already animating from a previous state, we can intercept.
114             return true;
115         }
116         if (AbstractFloatingView.getTopOpenViewWithType(
117                 mContainer, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) {
118             return false;
119         }
120         return isRecentsInteractive();
121     }
122 
isRecentsInteractive()123     protected abstract boolean isRecentsInteractive();
124 
125     /** Is recents view showing a single task in a modal way. */
isRecentsModal()126     protected abstract boolean isRecentsModal();
127 
onUserControlledAnimationCreated(AnimatorPlaybackController animController)128     protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
129     }
130 
131     @Override
onAnimationCancel(Animator animation)132     public void onAnimationCancel(Animator animation) {
133         if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
134             clearState();
135         }
136     }
137 
138     @Override
onControllerInterceptTouchEvent(MotionEvent ev)139     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
140         if ((ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL)
141                 && mCurrentAnimation == null) {
142             clearState();
143         }
144         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
145             mNoIntercept = !canInterceptTouch(ev);
146             if (mNoIntercept) {
147                 return false;
148             }
149 
150             // Now figure out which direction scroll events the controller will start
151             // calling the callbacks.
152             int directionsToDetectScroll = 0;
153             boolean ignoreSlopWhenSettling = false;
154             if (mCurrentAnimation != null) {
155                 directionsToDetectScroll = DIRECTION_BOTH;
156                 ignoreSlopWhenSettling = true;
157             } else {
158                 mTaskBeingDragged = null;
159 
160                 for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) {
161                     TaskView view = mRecentsView.getTaskViewAt(i);
162 
163                     if (mRecentsView.isTaskViewVisible(view) && mContainer.getDragLayer()
164                             .isEventOverView(view, ev)) {
165                         // Disable swiping up and down if the task overlay is modal.
166                         if (isRecentsModal()) {
167                             mTaskBeingDragged = null;
168                             break;
169                         }
170                         mTaskBeingDragged = view;
171                         int upDirection = mRecentsView.getPagedOrientationHandler()
172                                 .getUpDirection(mIsRtl);
173 
174                         // The task can be dragged up to dismiss it
175                         mAllowGoingUp = true;
176 
177                         // The task can be dragged down to open it if:
178                         // - It's the current page
179                         // - We support gestures to enter overview
180                         // - It's the focused task if in grid view
181                         // - The task is snapped
182                         mAllowGoingDown = i == mRecentsView.getCurrentPage()
183                                 && DisplayController.getNavigationMode(mContainer).hasGestures
184                                 && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask())
185                                 && mRecentsView.isTaskInExpectedScrollPosition(i);
186 
187                         directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection;
188                         break;
189                     }
190                 }
191                 if (mTaskBeingDragged == null) {
192                     mNoIntercept = true;
193                     return false;
194                 }
195             }
196 
197             mDetector.setDetectableScrollConditions(
198                     directionsToDetectScroll, ignoreSlopWhenSettling);
199         }
200 
201         if (mNoIntercept) {
202             return false;
203         }
204 
205         onControllerTouchEvent(ev);
206         return mDetector.isDraggingOrSettling();
207     }
208 
209     @Override
onControllerTouchEvent(MotionEvent ev)210     public boolean onControllerTouchEvent(MotionEvent ev) {
211         return mDetector.onTouchEvent(ev);
212     }
213 
reInitAnimationController(boolean goingUp)214     private void reInitAnimationController(boolean goingUp) {
215         if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
216             // No need to init
217             return;
218         }
219         if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) {
220             // Trying to re-init in an unsupported direction.
221             return;
222         }
223         if (mCurrentAnimation != null) {
224             mCurrentAnimation.setPlayFraction(0);
225             mCurrentAnimation.getTarget().removeListener(this);
226             mCurrentAnimation.dispatchOnCancel();
227         }
228 
229         RecentsPagedOrientationHandler orientationHandler =
230                 mRecentsView.getPagedOrientationHandler();
231         mCurrentAnimationIsGoingUp = goingUp;
232         BaseDragLayer dl = mContainer.getDragLayer();
233         final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
234         long maxDuration = 2 * secondaryLayerDimension;
235         int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
236         int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
237         // The interpolator controlling the most prominent visual movement. We use this to determine
238         // whether we passed SUCCESS_TRANSITION_PROGRESS.
239         final Interpolator currentInterpolator;
240         PendingAnimation pa;
241         if (goingUp) {
242             currentInterpolator = Interpolators.LINEAR;
243             pa = new PendingAnimation(maxDuration);
244             mRecentsView.createTaskDismissAnimation(pa, mTaskBeingDragged,
245                     true /* animateTaskView */, true /* removeTask */, maxDuration,
246                     false /* dismissingForSplitSelection*/);
247 
248             mEndDisplacement = -secondaryTaskDimension;
249         } else {
250             currentInterpolator = Interpolators.ZOOM_IN;
251             pa = mRecentsView.createTaskLaunchAnimation(
252                     mTaskBeingDragged, maxDuration, currentInterpolator);
253 
254             // Since the thumbnail is what is filling the screen, based the end displacement on it.
255             View thumbnailView = mTaskBeingDragged.getFirstThumbnailViewDeprecated();
256             mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
257             dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
258             mEndDisplacement = secondaryLayerDimension - mTempCords[1];
259         }
260         mEndDisplacement *= verticalFactor;
261         mCurrentAnimation = pa.createPlaybackController();
262 
263         // Setting this interpolator doesn't affect the visual motion, but is used to determine
264         // whether we successfully reached the target state in onDragEnd().
265         mCurrentAnimation.getTarget().setInterpolator(currentInterpolator);
266         onUserControlledAnimationCreated(mCurrentAnimation);
267         mCurrentAnimation.getTarget().addListener(this);
268         mCurrentAnimation.dispatchOnStart();
269         mProgressMultiplier = 1 / mEndDisplacement;
270     }
271 
272     @Override
onDragStart(boolean start, float startDisplacement)273     public void onDragStart(boolean start, float startDisplacement) {
274         if (!mDraggingEnabled) return;
275 
276         RecentsPagedOrientationHandler orientationHandler =
277                 mRecentsView.getPagedOrientationHandler();
278         if (mCurrentAnimation == null) {
279             reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
280             mDisplacementShift = 0;
281         } else {
282             mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
283             mCurrentAnimation.pause();
284         }
285         mFlingBlockCheck.unblockFling();
286         mOverrideVelocity = null;
287     }
288 
289     @Override
onDrag(float displacement)290     public boolean onDrag(float displacement) {
291         if (!mDraggingEnabled) return true;
292 
293         RecentsPagedOrientationHandler orientationHandler =
294                 mRecentsView.getPagedOrientationHandler();
295         float totalDisplacement = displacement + mDisplacementShift;
296         boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
297                 orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
298         if (isGoingUp != mCurrentAnimationIsGoingUp) {
299             reInitAnimationController(isGoingUp);
300             mFlingBlockCheck.blockFling();
301         } else {
302             mFlingBlockCheck.onEvent();
303         }
304 
305         if (isGoingUp) {
306             if (mCurrentAnimation.getProgressFraction() < ANIMATION_PROGRESS_FRACTION_MIDPOINT) {
307                 // Halve the value when dismissing, as we are animating the drag across the full
308                 // length for only the first half of the progress
309                 mCurrentAnimation.setPlayFraction(
310                         Utilities.boundToRange(totalDisplacement * mProgressMultiplier / 2, 0, 1));
311             } else {
312                 // Set mOverrideVelocity to control task dismiss velocity in onDragEnd
313                 int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity;
314                 if (mRecentsView.showAsGrid()) {
315                     if (mTaskBeingDragged.isFocusedTask()) {
316                         velocityDimenId =
317                                 R.dimen.default_task_dismiss_drag_velocity_grid_focus_task;
318                     } else {
319                         velocityDimenId = R.dimen.default_task_dismiss_drag_velocity_grid;
320                     }
321                 }
322                 mOverrideVelocity = -mTaskBeingDragged.getResources().getDimension(velocityDimenId);
323 
324                 // Once halfway through task dismissal interpolation, switch from reversible
325                 // dragging-task animation to playing the remaining task translation animations,
326                 // while this is in progress disable dragging.
327                 mDraggingEnabled = false;
328             }
329         } else {
330             mCurrentAnimation.setPlayFraction(
331                     Utilities.boundToRange(totalDisplacement * mProgressMultiplier, 0, 1));
332         }
333 
334         return true;
335     }
336 
337     @Override
onDragEnd(float velocity)338     public void onDragEnd(float velocity) {
339         if (mOverrideVelocity != null) {
340             velocity = mOverrideVelocity;
341             mOverrideVelocity = null;
342         }
343         // Limit velocity, as very large scalar values make animations play too quickly
344         float maxTaskDismissDragVelocity = mTaskBeingDragged.getResources().getDimension(
345                 R.dimen.max_task_dismiss_drag_velocity);
346         velocity = Utilities.boundToRange(velocity, -maxTaskDismissDragVelocity,
347                 maxTaskDismissDragVelocity);
348         boolean fling = mDraggingEnabled && mDetector.isFling(velocity);
349         final boolean goingToEnd;
350         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
351         if (blockedFling) {
352             fling = false;
353         }
354         RecentsPagedOrientationHandler orientationHandler =
355                 mRecentsView.getPagedOrientationHandler();
356         boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
357         float progress = mCurrentAnimation.getProgressFraction();
358         float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
359         if (fling) {
360             goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
361         } else {
362             goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
363         }
364         long animationDuration = BaseSwipeDetector.calculateDuration(
365                 velocity, goingToEnd ? (1 - progress) : progress);
366         if (blockedFling && !goingToEnd) {
367             animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
368         }
369         // Due to very high or low velocity dismissals, animation durations can be inconsistently
370         // long or short. Bound the duration for animation of task translations for a more
371         // standardized feel.
372         animationDuration = Utilities.boundToRange(animationDuration,
373                 MIN_TASK_DISMISS_ANIMATION_DURATION, MAX_TASK_DISMISS_ANIMATION_DURATION);
374 
375         mCurrentAnimation.setEndAction(this::clearState);
376         mCurrentAnimation.startWithVelocity(mContainer, goingToEnd, Math.abs(velocity),
377                 mEndDisplacement, animationDuration);
378         if (goingUp && goingToEnd && !mIsDismissHapticRunning) {
379             VibratorWrapper.INSTANCE.get(mContainer).vibrate(TASK_DISMISS_VIBRATION_PRIMITIVE,
380                     TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE, TASK_DISMISS_VIBRATION_FALLBACK);
381             mIsDismissHapticRunning = true;
382         }
383 
384         mDraggingEnabled = true;
385     }
386 
clearState()387     private void clearState() {
388         mDetector.finishedScrolling();
389         mDetector.setDetectableScrollConditions(0, false);
390         mDraggingEnabled = true;
391         mTaskBeingDragged = null;
392         mCurrentAnimation = null;
393         mIsDismissHapticRunning = false;
394     }
395 }
396