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