1 /*
2  * Copyright (C) 2011 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.systemui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.ValueAnimator;
23 import android.animation.ValueAnimator.AnimatorUpdateListener;
24 import android.content.Context;
25 import android.graphics.RectF;
26 import android.os.Handler;
27 import android.util.Log;
28 import android.view.MotionEvent;
29 import android.view.VelocityTracker;
30 import android.view.View;
31 import android.view.ViewConfiguration;
32 import android.view.accessibility.AccessibilityEvent;
33 import android.view.animation.AnimationUtils;
34 import android.view.animation.Interpolator;
35 import android.view.animation.LinearInterpolator;
36 
37 public class SwipeHelper implements Gefingerpoken {
38     static final String TAG = "com.android.systemui.SwipeHelper";
39     private static final boolean DEBUG = false;
40     private static final boolean DEBUG_INVALIDATE = false;
41     private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
42     private static final boolean CONSTRAIN_SWIPE = true;
43     private static final boolean FADE_OUT_DURING_SWIPE = true;
44     private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
45 
46     public static final int X = 0;
47     public static final int Y = 1;
48 
49     private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
50     private final Interpolator mFastOutLinearInInterpolator;
51 
52     private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
53     private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
54     private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
55     private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
56     private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
57 
58     public static float SWIPE_PROGRESS_FADE_START = 0f; // fraction of thumbnail width
59                                                  // where fade starts
60     static final float SWIPE_PROGRESS_FADE_END = 0.5f; // fraction of thumbnail width
61                                               // beyond which swipe progress->0
62     private float mMinSwipeProgress = 0f;
63     private float mMaxSwipeProgress = 1f;
64 
65     private float mPagingTouchSlop;
66     private Callback mCallback;
67     private Handler mHandler;
68     private int mSwipeDirection;
69     private VelocityTracker mVelocityTracker;
70 
71     private float mInitialTouchPos;
72     private boolean mDragging;
73     private View mCurrView;
74     private View mCurrAnimView;
75     private boolean mCanCurrViewBeDimissed;
76     private float mDensityScale;
77 
78     private boolean mLongPressSent;
79     private LongPressListener mLongPressListener;
80     private Runnable mWatchLongPress;
81     private long mLongPressTimeout;
82 
83     final private int[] mTmpPos = new int[2];
84     private int mFalsingThreshold;
85     private boolean mTouchAboveFalsingThreshold;
86 
SwipeHelper(int swipeDirection, Callback callback, Context context)87     public SwipeHelper(int swipeDirection, Callback callback, Context context) {
88         mCallback = callback;
89         mHandler = new Handler();
90         mSwipeDirection = swipeDirection;
91         mVelocityTracker = VelocityTracker.obtain();
92         mDensityScale =  context.getResources().getDisplayMetrics().density;
93         mPagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
94 
95         mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press!
96         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
97                 android.R.interpolator.fast_out_linear_in);
98         mFalsingThreshold = context.getResources().getDimensionPixelSize(
99                 R.dimen.swipe_helper_falsing_threshold);
100     }
101 
setLongPressListener(LongPressListener listener)102     public void setLongPressListener(LongPressListener listener) {
103         mLongPressListener = listener;
104     }
105 
setDensityScale(float densityScale)106     public void setDensityScale(float densityScale) {
107         mDensityScale = densityScale;
108     }
109 
setPagingTouchSlop(float pagingTouchSlop)110     public void setPagingTouchSlop(float pagingTouchSlop) {
111         mPagingTouchSlop = pagingTouchSlop;
112     }
113 
getPos(MotionEvent ev)114     private float getPos(MotionEvent ev) {
115         return mSwipeDirection == X ? ev.getX() : ev.getY();
116     }
117 
getTranslation(View v)118     private float getTranslation(View v) {
119         return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
120     }
121 
getVelocity(VelocityTracker vt)122     private float getVelocity(VelocityTracker vt) {
123         return mSwipeDirection == X ? vt.getXVelocity() :
124                 vt.getYVelocity();
125     }
126 
createTranslationAnimation(View v, float newPos)127     private ObjectAnimator createTranslationAnimation(View v, float newPos) {
128         ObjectAnimator anim = ObjectAnimator.ofFloat(v,
129                 mSwipeDirection == X ? "translationX" : "translationY", newPos);
130         return anim;
131     }
132 
getPerpendicularVelocity(VelocityTracker vt)133     private float getPerpendicularVelocity(VelocityTracker vt) {
134         return mSwipeDirection == X ? vt.getYVelocity() :
135                 vt.getXVelocity();
136     }
137 
setTranslation(View v, float translate)138     private void setTranslation(View v, float translate) {
139         if (mSwipeDirection == X) {
140             v.setTranslationX(translate);
141         } else {
142             v.setTranslationY(translate);
143         }
144     }
145 
getSize(View v)146     private float getSize(View v) {
147         return mSwipeDirection == X ? v.getMeasuredWidth() :
148                 v.getMeasuredHeight();
149     }
150 
setMinSwipeProgress(float minSwipeProgress)151     public void setMinSwipeProgress(float minSwipeProgress) {
152         mMinSwipeProgress = minSwipeProgress;
153     }
154 
setMaxSwipeProgress(float maxSwipeProgress)155     public void setMaxSwipeProgress(float maxSwipeProgress) {
156         mMaxSwipeProgress = maxSwipeProgress;
157     }
158 
getSwipeProgressForOffset(View view)159     private float getSwipeProgressForOffset(View view) {
160         float viewSize = getSize(view);
161         final float fadeSize = SWIPE_PROGRESS_FADE_END * viewSize;
162         float result = 1.0f;
163         float pos = getTranslation(view);
164         if (pos >= viewSize * SWIPE_PROGRESS_FADE_START) {
165             result = 1.0f - (pos - viewSize * SWIPE_PROGRESS_FADE_START) / fadeSize;
166         } else if (pos < viewSize * (1.0f - SWIPE_PROGRESS_FADE_START)) {
167             result = 1.0f + (viewSize * SWIPE_PROGRESS_FADE_START + pos) / fadeSize;
168         }
169         return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress);
170     }
171 
updateSwipeProgressFromOffset(View animView, boolean dismissable)172     private void updateSwipeProgressFromOffset(View animView, boolean dismissable) {
173         float swipeProgress = getSwipeProgressForOffset(animView);
174         if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) {
175             if (FADE_OUT_DURING_SWIPE && dismissable) {
176                 float alpha = swipeProgress;
177                 if (alpha != 0f && alpha != 1f) {
178                     animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
179                 } else {
180                     animView.setLayerType(View.LAYER_TYPE_NONE, null);
181                 }
182                 animView.setAlpha(getSwipeProgressForOffset(animView));
183             }
184         }
185         invalidateGlobalRegion(animView);
186     }
187 
188     // invalidate the view's own bounds all the way up the view hierarchy
invalidateGlobalRegion(View view)189     public static void invalidateGlobalRegion(View view) {
190         invalidateGlobalRegion(
191             view,
192             new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
193     }
194 
195     // invalidate a rectangle relative to the view's coordinate system all the way up the view
196     // hierarchy
invalidateGlobalRegion(View view, RectF childBounds)197     public static void invalidateGlobalRegion(View view, RectF childBounds) {
198         //childBounds.offset(view.getTranslationX(), view.getTranslationY());
199         if (DEBUG_INVALIDATE)
200             Log.v(TAG, "-------------");
201         while (view.getParent() != null && view.getParent() instanceof View) {
202             view = (View) view.getParent();
203             view.getMatrix().mapRect(childBounds);
204             view.invalidate((int) Math.floor(childBounds.left),
205                             (int) Math.floor(childBounds.top),
206                             (int) Math.ceil(childBounds.right),
207                             (int) Math.ceil(childBounds.bottom));
208             if (DEBUG_INVALIDATE) {
209                 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
210                         + "," + (int) Math.floor(childBounds.top)
211                         + "," + (int) Math.ceil(childBounds.right)
212                         + "," + (int) Math.ceil(childBounds.bottom));
213             }
214         }
215     }
216 
removeLongPressCallback()217     public void removeLongPressCallback() {
218         if (mWatchLongPress != null) {
219             mHandler.removeCallbacks(mWatchLongPress);
220             mWatchLongPress = null;
221         }
222     }
223 
onInterceptTouchEvent(final MotionEvent ev)224     public boolean onInterceptTouchEvent(final MotionEvent ev) {
225         final int action = ev.getAction();
226 
227         switch (action) {
228             case MotionEvent.ACTION_DOWN:
229                 mTouchAboveFalsingThreshold = false;
230                 mDragging = false;
231                 mLongPressSent = false;
232                 mCurrView = mCallback.getChildAtPosition(ev);
233                 mVelocityTracker.clear();
234                 if (mCurrView != null) {
235                     mCurrAnimView = mCallback.getChildContentView(mCurrView);
236                     mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
237                     mVelocityTracker.addMovement(ev);
238                     mInitialTouchPos = getPos(ev);
239 
240                     if (mLongPressListener != null) {
241                         if (mWatchLongPress == null) {
242                             mWatchLongPress = new Runnable() {
243                                 @Override
244                                 public void run() {
245                                     if (mCurrView != null && !mLongPressSent) {
246                                         mLongPressSent = true;
247                                         mCurrView.sendAccessibilityEvent(
248                                                 AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
249                                         mCurrView.getLocationOnScreen(mTmpPos);
250                                         final int x = (int) ev.getRawX() - mTmpPos[0];
251                                         final int y = (int) ev.getRawY() - mTmpPos[1];
252                                         mLongPressListener.onLongPress(mCurrView, x, y);
253                                     }
254                                 }
255                             };
256                         }
257                         mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
258                     }
259 
260                 }
261                 break;
262 
263             case MotionEvent.ACTION_MOVE:
264                 if (mCurrView != null && !mLongPressSent) {
265                     mVelocityTracker.addMovement(ev);
266                     float pos = getPos(ev);
267                     float delta = pos - mInitialTouchPos;
268                     if (Math.abs(delta) > mPagingTouchSlop) {
269                         mCallback.onBeginDrag(mCurrView);
270                         mDragging = true;
271                         mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
272 
273                         removeLongPressCallback();
274                     }
275                 }
276 
277                 break;
278 
279             case MotionEvent.ACTION_UP:
280             case MotionEvent.ACTION_CANCEL:
281                 final boolean captured = (mDragging || mLongPressSent);
282                 mDragging = false;
283                 mCurrView = null;
284                 mCurrAnimView = null;
285                 mLongPressSent = false;
286                 removeLongPressCallback();
287                 if (captured) return true;
288                 break;
289         }
290         return mDragging || mLongPressSent;
291     }
292 
293     /**
294      * @param view The view to be dismissed
295      * @param velocity The desired pixels/second speed at which the view should move
296      */
dismissChild(final View view, float velocity)297     public void dismissChild(final View view, float velocity) {
298         dismissChild(view, velocity, null, 0, false, 0);
299     }
300 
301     /**
302      * @param view The view to be dismissed
303      * @param velocity The desired pixels/second speed at which the view should move
304      * @param endAction The action to perform at the end
305      * @param delay The delay after which we should start
306      * @param useAccelerateInterpolator Should an accelerating Interpolator be used
307      * @param fixedDuration If not 0, this exact duration will be taken
308      */
dismissChild(final View view, float velocity, final Runnable endAction, long delay, boolean useAccelerateInterpolator, long fixedDuration)309     public void dismissChild(final View view, float velocity, final Runnable endAction,
310             long delay, boolean useAccelerateInterpolator, long fixedDuration) {
311         final View animView = mCallback.getChildContentView(view);
312         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
313         float newPos;
314         boolean isLayoutRtl = view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
315 
316         if (velocity < 0
317                 || (velocity == 0 && getTranslation(animView) < 0)
318                 // if we use the Menu to dismiss an item in landscape, animate up
319                 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)
320                 // if the language is rtl we prefer swiping to the left
321                 || (velocity == 0 && getTranslation(animView) == 0 && isLayoutRtl)) {
322             newPos = -getSize(animView);
323         } else {
324             newPos = getSize(animView);
325         }
326         long duration;
327         if (fixedDuration == 0) {
328             duration = MAX_ESCAPE_ANIMATION_DURATION;
329             if (velocity != 0) {
330                 duration = Math.min(duration,
331                         (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
332                                 .abs(velocity))
333                 );
334             } else {
335                 duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
336             }
337         } else {
338             duration = fixedDuration;
339         }
340 
341         animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
342         ObjectAnimator anim = createTranslationAnimation(animView, newPos);
343         if (useAccelerateInterpolator) {
344             anim.setInterpolator(mFastOutLinearInInterpolator);
345         } else {
346             anim.setInterpolator(sLinearInterpolator);
347         }
348         anim.setDuration(duration);
349         if (delay > 0) {
350             anim.setStartDelay(delay);
351         }
352         anim.addListener(new AnimatorListenerAdapter() {
353             public void onAnimationEnd(Animator animation) {
354                 mCallback.onChildDismissed(view);
355                 if (endAction != null) {
356                     endAction.run();
357                 }
358                 animView.setLayerType(View.LAYER_TYPE_NONE, null);
359             }
360         });
361         anim.addUpdateListener(new AnimatorUpdateListener() {
362             public void onAnimationUpdate(ValueAnimator animation) {
363                 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
364             }
365         });
366         anim.start();
367     }
368 
snapChild(final View view, float velocity)369     public void snapChild(final View view, float velocity) {
370         final View animView = mCallback.getChildContentView(view);
371         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
372         ObjectAnimator anim = createTranslationAnimation(animView, 0);
373         int duration = SNAP_ANIM_LEN;
374         anim.setDuration(duration);
375         anim.addUpdateListener(new AnimatorUpdateListener() {
376             public void onAnimationUpdate(ValueAnimator animation) {
377                 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
378             }
379         });
380         anim.addListener(new AnimatorListenerAdapter() {
381             public void onAnimationEnd(Animator animator) {
382                 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
383                 mCallback.onChildSnappedBack(animView);
384             }
385         });
386         anim.start();
387     }
388 
onTouchEvent(MotionEvent ev)389     public boolean onTouchEvent(MotionEvent ev) {
390         if (mLongPressSent) {
391             return true;
392         }
393 
394         if (!mDragging) {
395             if (mCallback.getChildAtPosition(ev) != null) {
396 
397                 // We are dragging directly over a card, make sure that we also catch the gesture
398                 // even if nobody else wants the touch event.
399                 onInterceptTouchEvent(ev);
400                 return true;
401             } else {
402 
403                 // We are not doing anything, make sure the long press callback
404                 // is not still ticking like a bomb waiting to go off.
405                 removeLongPressCallback();
406                 return false;
407             }
408         }
409 
410         mVelocityTracker.addMovement(ev);
411         final int action = ev.getAction();
412         switch (action) {
413             case MotionEvent.ACTION_OUTSIDE:
414             case MotionEvent.ACTION_MOVE:
415                 if (mCurrView != null) {
416                     float delta = getPos(ev) - mInitialTouchPos;
417                     float absDelta = Math.abs(delta);
418                     if (absDelta >= getFalsingThreshold()) {
419                         mTouchAboveFalsingThreshold = true;
420                     }
421                     // don't let items that can't be dismissed be dragged more than
422                     // maxScrollDistance
423                     if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
424                         float size = getSize(mCurrAnimView);
425                         float maxScrollDistance = 0.15f * size;
426                         if (absDelta >= size) {
427                             delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
428                         } else {
429                             delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
430                         }
431                     }
432                     setTranslation(mCurrAnimView, delta);
433 
434                     updateSwipeProgressFromOffset(mCurrAnimView, mCanCurrViewBeDimissed);
435                 }
436                 break;
437             case MotionEvent.ACTION_UP:
438             case MotionEvent.ACTION_CANCEL:
439                 if (mCurrView != null) {
440                     float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
441                     mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
442                     float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
443                     float velocity = getVelocity(mVelocityTracker);
444                     float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
445 
446                     // Decide whether to dismiss the current view
447                     boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
448                             Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
449                     boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
450                             (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
451                             (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
452                     boolean falsingDetected = mCallback.isAntiFalsingNeeded()
453                             && !mTouchAboveFalsingThreshold;
454 
455                     boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
456                             && !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough)
457                             && ev.getActionMasked() == MotionEvent.ACTION_UP;
458 
459                     if (dismissChild) {
460                         // flingadingy
461                         dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
462                     } else {
463                         // snappity
464                         mCallback.onDragCancelled(mCurrView);
465                         snapChild(mCurrView, velocity);
466                     }
467                 }
468                 break;
469         }
470         return true;
471     }
472 
getFalsingThreshold()473     private int getFalsingThreshold() {
474         float factor = mCallback.getFalsingThresholdFactor();
475         return (int) (mFalsingThreshold * factor);
476     }
477 
478     public interface Callback {
getChildAtPosition(MotionEvent ev)479         View getChildAtPosition(MotionEvent ev);
480 
getChildContentView(View v)481         View getChildContentView(View v);
482 
canChildBeDismissed(View v)483         boolean canChildBeDismissed(View v);
484 
isAntiFalsingNeeded()485         boolean isAntiFalsingNeeded();
486 
onBeginDrag(View v)487         void onBeginDrag(View v);
488 
onChildDismissed(View v)489         void onChildDismissed(View v);
490 
onDragCancelled(View v)491         void onDragCancelled(View v);
492 
onChildSnappedBack(View animView)493         void onChildSnappedBack(View animView);
494 
495         /**
496          * Updates the swipe progress on a child.
497          *
498          * @return if true, prevents the default alpha fading.
499          */
updateSwipeProgress(View animView, boolean dismissable, float swipeProgress)500         boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress);
501 
502         /**
503          * @return The factor the falsing threshold should be multiplied with
504          */
getFalsingThresholdFactor()505         float getFalsingThresholdFactor();
506     }
507 
508     /**
509      * Equivalent to View.OnLongClickListener with coordinates
510      */
511     public interface LongPressListener {
512         /**
513          * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
514          * @return whether the longpress was handled
515          */
onLongPress(View v, int x, int y)516         boolean onLongPress(View v, int x, int y);
517     }
518 }
519