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