• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
315         if (velocity < 0
316                 || (velocity == 0 && getTranslation(animView) < 0)
317                 // if we use the Menu to dismiss an item in landscape, animate up
318                 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
319             newPos = -getSize(animView);
320         } else {
321             newPos = getSize(animView);
322         }
323         long duration;
324         if (fixedDuration == 0) {
325             duration = MAX_ESCAPE_ANIMATION_DURATION;
326             if (velocity != 0) {
327                 duration = Math.min(duration,
328                         (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
329                                 .abs(velocity))
330                 );
331             } else {
332                 duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
333             }
334         } else {
335             duration = fixedDuration;
336         }
337 
338         animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
339         ObjectAnimator anim = createTranslationAnimation(animView, newPos);
340         if (useAccelerateInterpolator) {
341             anim.setInterpolator(mFastOutLinearInInterpolator);
342         } else {
343             anim.setInterpolator(sLinearInterpolator);
344         }
345         anim.setDuration(duration);
346         if (delay > 0) {
347             anim.setStartDelay(delay);
348         }
349         anim.addListener(new AnimatorListenerAdapter() {
350             public void onAnimationEnd(Animator animation) {
351                 mCallback.onChildDismissed(view);
352                 if (endAction != null) {
353                     endAction.run();
354                 }
355                 animView.setLayerType(View.LAYER_TYPE_NONE, null);
356             }
357         });
358         anim.addUpdateListener(new AnimatorUpdateListener() {
359             public void onAnimationUpdate(ValueAnimator animation) {
360                 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
361             }
362         });
363         anim.start();
364     }
365 
snapChild(final View view, float velocity)366     public void snapChild(final View view, float velocity) {
367         final View animView = mCallback.getChildContentView(view);
368         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
369         ObjectAnimator anim = createTranslationAnimation(animView, 0);
370         int duration = SNAP_ANIM_LEN;
371         anim.setDuration(duration);
372         anim.addUpdateListener(new AnimatorUpdateListener() {
373             public void onAnimationUpdate(ValueAnimator animation) {
374                 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
375             }
376         });
377         anim.addListener(new AnimatorListenerAdapter() {
378             public void onAnimationEnd(Animator animator) {
379                 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
380                 mCallback.onChildSnappedBack(animView);
381             }
382         });
383         anim.start();
384     }
385 
onTouchEvent(MotionEvent ev)386     public boolean onTouchEvent(MotionEvent ev) {
387         if (mLongPressSent) {
388             return true;
389         }
390 
391         if (!mDragging) {
392             if (mCallback.getChildAtPosition(ev) != null) {
393 
394                 // We are dragging directly over a card, make sure that we also catch the gesture
395                 // even if nobody else wants the touch event.
396                 onInterceptTouchEvent(ev);
397                 return true;
398             } else {
399 
400                 // We are not doing anything, make sure the long press callback
401                 // is not still ticking like a bomb waiting to go off.
402                 removeLongPressCallback();
403                 return false;
404             }
405         }
406 
407         mVelocityTracker.addMovement(ev);
408         final int action = ev.getAction();
409         switch (action) {
410             case MotionEvent.ACTION_OUTSIDE:
411             case MotionEvent.ACTION_MOVE:
412                 if (mCurrView != null) {
413                     float delta = getPos(ev) - mInitialTouchPos;
414                     float absDelta = Math.abs(delta);
415                     if (absDelta >= getFalsingThreshold()) {
416                         mTouchAboveFalsingThreshold = true;
417                     }
418                     // don't let items that can't be dismissed be dragged more than
419                     // maxScrollDistance
420                     if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
421                         float size = getSize(mCurrAnimView);
422                         float maxScrollDistance = 0.15f * size;
423                         if (absDelta >= size) {
424                             delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
425                         } else {
426                             delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
427                         }
428                     }
429                     setTranslation(mCurrAnimView, delta);
430 
431                     updateSwipeProgressFromOffset(mCurrAnimView, mCanCurrViewBeDimissed);
432                 }
433                 break;
434             case MotionEvent.ACTION_UP:
435             case MotionEvent.ACTION_CANCEL:
436                 if (mCurrView != null) {
437                     float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
438                     mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
439                     float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
440                     float velocity = getVelocity(mVelocityTracker);
441                     float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
442 
443                     // Decide whether to dismiss the current view
444                     boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
445                             Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
446                     boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
447                             (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
448                             (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
449                     boolean falsingDetected = mCallback.isAntiFalsingNeeded()
450                             && !mTouchAboveFalsingThreshold;
451 
452                     boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
453                             && !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough);
454 
455                     if (dismissChild) {
456                         // flingadingy
457                         dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
458                     } else {
459                         // snappity
460                         mCallback.onDragCancelled(mCurrView);
461                         snapChild(mCurrView, velocity);
462                     }
463                 }
464                 break;
465         }
466         return true;
467     }
468 
getFalsingThreshold()469     private int getFalsingThreshold() {
470         float factor = mCallback.getFalsingThresholdFactor();
471         return (int) (mFalsingThreshold * factor);
472     }
473 
474     public interface Callback {
getChildAtPosition(MotionEvent ev)475         View getChildAtPosition(MotionEvent ev);
476 
getChildContentView(View v)477         View getChildContentView(View v);
478 
canChildBeDismissed(View v)479         boolean canChildBeDismissed(View v);
480 
isAntiFalsingNeeded()481         boolean isAntiFalsingNeeded();
482 
onBeginDrag(View v)483         void onBeginDrag(View v);
484 
onChildDismissed(View v)485         void onChildDismissed(View v);
486 
onDragCancelled(View v)487         void onDragCancelled(View v);
488 
onChildSnappedBack(View animView)489         void onChildSnappedBack(View animView);
490 
491         /**
492          * Updates the swipe progress on a child.
493          *
494          * @return if true, prevents the default alpha fading.
495          */
updateSwipeProgress(View animView, boolean dismissable, float swipeProgress)496         boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress);
497 
498         /**
499          * @return The factor the falsing threshold should be multiplied with
500          */
getFalsingThresholdFactor()501         float getFalsingThresholdFactor();
502     }
503 
504     /**
505      * Equivalent to View.OnLongClickListener with coordinates
506      */
507     public interface LongPressListener {
508         /**
509          * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
510          * @return whether the longpress was handled
511          */
onLongPress(View v, int x, int y)512         boolean onLongPress(View v, int x, int y);
513     }
514 }
515