• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 Google Inc.
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.dialer.list;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ObjectAnimator;
23 import android.animation.ValueAnimator;
24 import android.animation.ValueAnimator.AnimatorUpdateListener;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.graphics.RectF;
28 import android.util.Log;
29 import android.view.MotionEvent;
30 import android.view.VelocityTracker;
31 import android.view.View;
32 import android.view.animation.LinearInterpolator;
33 
34 import com.android.dialer.R;
35 
36 /**
37  * Copy of packages/apps/UnifiedEmail - com.android.mail.ui.SwipeHelper with changes.
38  */
39 public class SwipeHelper {
40     static final String TAG = SwipeHelper.class.getSimpleName();
41     private static final boolean DEBUG_INVALIDATE = false;
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     private static final boolean LOG_SWIPE_DISMISS_VELOCITY = false; // STOPSHIP - DEBUG ONLY
46 
47     public static final int IS_SWIPEABLE_TAG = R.id.is_swipeable_tag;
48     public static final Object IS_SWIPEABLE = new Object();
49 
50     public static final int X = 0;
51     public static final int Y = 1;
52 
53     private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
54 
55     private static int SWIPE_ESCAPE_VELOCITY = -1;
56     private static int DEFAULT_ESCAPE_ANIMATION_DURATION;
57     private static int MAX_ESCAPE_ANIMATION_DURATION;
58     private static int MAX_DISMISS_VELOCITY;
59     private static int SNAP_ANIM_LEN;
60     private static int SWIPE_SCROLL_SLOP;
61     private static float MIN_SWIPE;
62     private static float MIN_VERT;
63     private static float MIN_LOCK;
64 
65     public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
66                                                // where fade starts
67     static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width
68                                               // beyond which alpha->0
69     private static final float FACTOR = 1.2f;
70 
71     private static final int PROTECTION_PADDING = 50;
72 
73     private float mMinAlpha = 0.3f;
74 
75     private float mPagingTouchSlop;
76     private final SwipeHelperCallback mCallback;
77     private final int mSwipeDirection;
78     private final VelocityTracker mVelocityTracker;
79 
80     private float mInitialTouchPosX;
81     private boolean mDragging;
82     private View mCurrView;
83     private View mCurrAnimView;
84     private boolean mCanCurrViewBeDimissed;
85     private float mDensityScale;
86     private float mLastY;
87     private float mInitialTouchPosY;
88 
89     private float mStartAlpha;
90     private boolean mProtected = false;
91 
92     private float mChildSwipedFarEnoughFactor = 0.4f;
93     private float mChildSwipedFastEnoughFactor = 0.05f;
94 
SwipeHelper(Context context, int swipeDirection, SwipeHelperCallback callback, float densityScale, float pagingTouchSlop)95     public SwipeHelper(Context context, int swipeDirection, SwipeHelperCallback callback, float densityScale,
96             float pagingTouchSlop) {
97         mCallback = callback;
98         mSwipeDirection = swipeDirection;
99         mVelocityTracker = VelocityTracker.obtain();
100         mDensityScale = densityScale;
101         mPagingTouchSlop = pagingTouchSlop;
102         if (SWIPE_ESCAPE_VELOCITY == -1) {
103             Resources res = context.getResources();
104             SWIPE_ESCAPE_VELOCITY = res.getInteger(R.integer.swipe_escape_velocity);
105             DEFAULT_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.escape_animation_duration);
106             MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration);
107             MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity);
108             SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration);
109             SWIPE_SCROLL_SLOP = res.getInteger(R.integer.swipe_scroll_slop);
110             MIN_SWIPE = res.getDimension(R.dimen.min_swipe);
111             MIN_VERT = res.getDimension(R.dimen.min_vert);
112             MIN_LOCK = res.getDimension(R.dimen.min_lock);
113         }
114     }
115 
setDensityScale(float densityScale)116     public void setDensityScale(float densityScale) {
117         mDensityScale = densityScale;
118     }
119 
setPagingTouchSlop(float pagingTouchSlop)120     public void setPagingTouchSlop(float pagingTouchSlop) {
121         mPagingTouchSlop = pagingTouchSlop;
122     }
123 
setChildSwipedFarEnoughFactor(float factor)124     public void setChildSwipedFarEnoughFactor(float factor) {
125         mChildSwipedFarEnoughFactor = factor;
126     }
127 
setChildSwipedFastEnoughFactor(float factor)128     public void setChildSwipedFastEnoughFactor(float factor) {
129         mChildSwipedFastEnoughFactor = factor;
130     }
131 
getVelocity(VelocityTracker vt)132     private float getVelocity(VelocityTracker vt) {
133         return mSwipeDirection == X ? vt.getXVelocity() :
134                 vt.getYVelocity();
135     }
136 
createTranslationAnimation(View v, float newPos)137     private ObjectAnimator createTranslationAnimation(View v, float newPos) {
138         ObjectAnimator anim = ObjectAnimator.ofFloat(v,
139                 mSwipeDirection == X ? "translationX" : "translationY", newPos);
140         return anim;
141     }
142 
createDismissAnimation(View v, float newPos, int duration)143     private ObjectAnimator createDismissAnimation(View v, float newPos, int duration) {
144         ObjectAnimator anim = createTranslationAnimation(v, newPos);
145         anim.setInterpolator(sLinearInterpolator);
146         anim.setDuration(duration);
147         return anim;
148     }
149 
getPerpendicularVelocity(VelocityTracker vt)150     private float getPerpendicularVelocity(VelocityTracker vt) {
151         return mSwipeDirection == X ? vt.getYVelocity() :
152                 vt.getXVelocity();
153     }
154 
setTranslation(View v, float translate)155     private void setTranslation(View v, float translate) {
156         if (mSwipeDirection == X) {
157             v.setTranslationX(translate);
158         } else {
159             v.setTranslationY(translate);
160         }
161     }
162 
getSize(View v)163     private float getSize(View v) {
164         return mSwipeDirection == X ? v.getMeasuredWidth() :
165                 v.getMeasuredHeight();
166     }
167 
setMinAlpha(float minAlpha)168     public void setMinAlpha(float minAlpha) {
169         mMinAlpha = minAlpha;
170     }
171 
getAlphaForOffset(View view)172     private float getAlphaForOffset(View view) {
173         float viewSize = getSize(view);
174         final float fadeSize = ALPHA_FADE_END * viewSize;
175         float result = mStartAlpha;
176         float pos = view.getTranslationX();
177         if (pos >= viewSize * ALPHA_FADE_START) {
178             result = mStartAlpha - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
179         } else if (pos < viewSize * (mStartAlpha - ALPHA_FADE_START)) {
180             result = mStartAlpha + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
181         }
182         return Math.max(mMinAlpha, result);
183     }
184 
185     // invalidate the view's own bounds all the way up the view hierarchy
invalidateGlobalRegion(View view)186     public static void invalidateGlobalRegion(View view) {
187         invalidateGlobalRegion(
188                 view,
189                 new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
190     }
191 
192     // invalidate a rectangle relative to the view's coordinate system all the way up the view
193     // hierarchy
invalidateGlobalRegion(View view, RectF childBounds)194     public static void invalidateGlobalRegion(View view, RectF childBounds) {
195         // childBounds.offset(view.getTranslationX(), view.getTranslationY());
196         if (DEBUG_INVALIDATE)
197             Log.v(TAG, "-------------");
198         while (view.getParent() != null && view.getParent() instanceof View) {
199             view = (View) view.getParent();
200             view.getMatrix().mapRect(childBounds);
201             view.invalidate((int) Math.floor(childBounds.left),
202                     (int) Math.floor(childBounds.top),
203                     (int) Math.ceil(childBounds.right),
204                     (int) Math.ceil(childBounds.bottom));
205             if (DEBUG_INVALIDATE) {
206                 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
207                         + "," + (int) Math.floor(childBounds.top)
208                         + "," + (int) Math.ceil(childBounds.right)
209                         + "," + (int) Math.ceil(childBounds.bottom));
210             }
211         }
212     }
213 
onInterceptTouchEvent(MotionEvent ev)214     public boolean onInterceptTouchEvent(MotionEvent ev) {
215         final int action = ev.getAction();
216         switch (action) {
217             case MotionEvent.ACTION_DOWN:
218                 mLastY = ev.getY();
219                 mDragging = false;
220                 mCurrView = mCallback.getChildAtPosition(ev);
221                 mVelocityTracker.clear();
222                 if (mCurrView != null) {
223                     mCurrAnimView = mCallback.getChildContentView(mCurrView);
224                     mStartAlpha = mCurrAnimView.getAlpha();
225                     mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
226                     mVelocityTracker.addMovement(ev);
227                     mInitialTouchPosX = ev.getX();
228                     mInitialTouchPosY = ev.getY();
229                 }
230                 break;
231             case MotionEvent.ACTION_MOVE:
232                 if (mCurrView != null) {
233                     // Check the movement direction.
234                     if (mLastY >= 0 && !mDragging) {
235                         float currY = ev.getY();
236                         float currX = ev.getX();
237                         float deltaY = Math.abs(currY - mInitialTouchPosY);
238                         float deltaX = Math.abs(currX - mInitialTouchPosX);
239                         if (deltaY > SWIPE_SCROLL_SLOP && deltaY > (FACTOR * deltaX)) {
240                             mLastY = ev.getY();
241                             mCallback.onScroll();
242                             return false;
243                         }
244                     }
245                     mVelocityTracker.addMovement(ev);
246                     float pos = ev.getX();
247                     float delta = pos - mInitialTouchPosX;
248                     if (Math.abs(delta) > mPagingTouchSlop) {
249                         mCallback.onBeginDrag(mCallback.getChildContentView(mCurrView));
250                         mDragging = true;
251                         mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX();
252                         mInitialTouchPosY = ev.getY();
253                     }
254                 }
255                 mLastY = ev.getY();
256                 break;
257             case MotionEvent.ACTION_UP:
258             case MotionEvent.ACTION_CANCEL:
259                 mDragging = false;
260                 mCurrView = null;
261                 mCurrAnimView = null;
262                 mLastY = -1;
263                 break;
264         }
265         return mDragging;
266     }
267 
268     /**
269      * @param view The view to be dismissed
270      * @param velocity The desired pixels/second speed at which the view should
271      *            move
272      */
dismissChild(final View view, float velocity)273     private void dismissChild(final View view, float velocity) {
274         final View animView = mCallback.getChildContentView(view);
275         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
276         float newPos = determinePos(animView, velocity);
277         int duration = determineDuration(animView, newPos, velocity);
278 
279         animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
280         ObjectAnimator anim = createDismissAnimation(animView, newPos, duration);
281         anim.addListener(new AnimatorListenerAdapter() {
282             @Override
283             public void onAnimationEnd(Animator animation) {
284                 mCallback.onChildDismissed(view);
285                 animView.setLayerType(View.LAYER_TYPE_NONE, null);
286             }
287         });
288         anim.addUpdateListener(new AnimatorUpdateListener() {
289             @Override
290             public void onAnimationUpdate(ValueAnimator animation) {
291                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
292                     animView.setAlpha(getAlphaForOffset(animView));
293                 }
294                 invalidateGlobalRegion(animView);
295             }
296         });
297         anim.start();
298     }
299 
determineDuration(View animView, float newPos, float velocity)300     private int determineDuration(View animView, float newPos, float velocity) {
301         int duration = MAX_ESCAPE_ANIMATION_DURATION;
302         if (velocity != 0) {
303             duration = Math
304                     .min(duration,
305                             (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math
306                                     .abs(velocity)));
307         } else {
308             duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
309         }
310         return duration;
311     }
312 
determinePos(View animView, float velocity)313     private float determinePos(View animView, float velocity) {
314         float newPos = 0;
315         if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0)
316                 // if we use the Menu to dismiss an item in landscape, animate up
317                 || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) {
318             newPos = -getSize(animView);
319         } else {
320             newPos = getSize(animView);
321         }
322         return newPos;
323     }
324 
snapChild(final View view, float velocity)325     public void snapChild(final View view, float velocity) {
326         final View animView = mCallback.getChildContentView(view);
327         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
328         ObjectAnimator anim = createTranslationAnimation(animView, 0);
329         int duration = SNAP_ANIM_LEN;
330         anim.setDuration(duration);
331         anim.addUpdateListener(new AnimatorUpdateListener() {
332             @Override
333             public void onAnimationUpdate(ValueAnimator animation) {
334                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
335                     animView.setAlpha(getAlphaForOffset(animView));
336                 }
337                 invalidateGlobalRegion(animView);
338             }
339         });
340         anim.addListener(new AnimatorListenerAdapter() {
341             @Override
342             public void onAnimationEnd(Animator animation) {
343                 animView.setAlpha(mStartAlpha);
344                 mCallback.onDragCancelled(mCurrView);
345             }
346         });
347         anim.start();
348     }
349 
onTouchEvent(MotionEvent ev)350     public boolean onTouchEvent(MotionEvent ev) {
351         if (!mDragging || mProtected) {
352             return false;
353         }
354         mVelocityTracker.addMovement(ev);
355         final int action = ev.getAction();
356         switch (action) {
357             case MotionEvent.ACTION_OUTSIDE:
358             case MotionEvent.ACTION_MOVE:
359                 if (mCurrView != null) {
360                     float deltaX = ev.getX() - mInitialTouchPosX;
361                     float deltaY = Math.abs(ev.getY() - mInitialTouchPosY);
362                     // If the user has gone vertical and not gone horizontalish AT
363                     // LEAST minBeforeLock, switch to scroll. Otherwise, cancel
364                     // the swipe.
365                     if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK
366                             && deltaY > (FACTOR * Math.abs(deltaX))) {
367                         mCallback.onScroll();
368                         return false;
369                     }
370                     float minDistance = MIN_SWIPE;
371                     if (Math.abs(deltaX) < minDistance) {
372                         // Don't start the drag until at least X distance has
373                         // occurred.
374                         return true;
375                     }
376                     // don't let items that can't be dismissed be dragged more
377                     // than maxScrollDistance
378                     if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
379                         float size = getSize(mCurrAnimView);
380                         float maxScrollDistance = 0.15f * size;
381                         if (Math.abs(deltaX) >= size) {
382                             deltaX = deltaX > 0 ? maxScrollDistance : -maxScrollDistance;
383                         } else {
384                             deltaX = maxScrollDistance
385                                     * (float) Math.sin((deltaX / size) * (Math.PI / 2));
386                         }
387                     }
388                     setTranslation(mCurrAnimView, deltaX);
389                     if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
390                         mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
391                     }
392                     invalidateGlobalRegion(mCallback.getChildContentView(mCurrView));
393                 }
394                 break;
395             case MotionEvent.ACTION_UP:
396             case MotionEvent.ACTION_CANCEL:
397                 if (mCurrView != null) {
398                     float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
399                     mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
400                     float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
401                     float velocity = getVelocity(mVelocityTracker);
402                     float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
403 
404                     // Decide whether to dismiss the current view
405                     // Tweak constants below as required to prevent erroneous
406                     // swipe/dismiss
407                     float translation = Math.abs(mCurrAnimView.getTranslationX());
408                     float currAnimViewSize = getSize(mCurrAnimView);
409                     // Long swipe = translation of {@link #mChildSwipedFarEnoughFactor} * width
410                     boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH
411                             && translation > mChildSwipedFarEnoughFactor * currAnimViewSize;
412                     // Fast swipe = > escapeVelocity and translation of
413                     // {@link #mChildSwipedFastEnoughFactor} * width
414                     boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity)
415                             && (Math.abs(velocity) > Math.abs(perpendicularVelocity))
416                             && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0)
417                             && translation > mChildSwipedFastEnoughFactor * currAnimViewSize;
418                     if (LOG_SWIPE_DISMISS_VELOCITY) {
419                         Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/"
420                                 + perpendicularVelocity + ", x: " + translation + "/"
421                                 + currAnimViewSize);
422                     }
423 
424                     boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
425                             && (childSwipedFastEnough || childSwipedFarEnough);
426 
427                     if (dismissChild) {
428                         dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
429                     } else {
430                         snapChild(mCurrView, velocity);
431                     }
432                 }
433                 break;
434         }
435         return true;
436     }
437 
setSwipeable(View view, boolean swipeable)438     public static void setSwipeable(View view, boolean swipeable) {
439         view.setTag(IS_SWIPEABLE_TAG, swipeable ? IS_SWIPEABLE : null);
440     }
441 
isSwipeable(View view)442     public static boolean isSwipeable(View view) {
443         return IS_SWIPEABLE == view.getTag(IS_SWIPEABLE_TAG);
444     }
445 
446     public interface SwipeHelperCallback {
getChildAtPosition(MotionEvent ev)447         View getChildAtPosition(MotionEvent ev);
448 
getChildContentView(View v)449         View getChildContentView(View v);
450 
onScroll()451         void onScroll();
452 
canChildBeDismissed(View v)453         boolean canChildBeDismissed(View v);
454 
onBeginDrag(View v)455         void onBeginDrag(View v);
456 
onChildDismissed(View v)457         void onChildDismissed(View v);
458 
onDragCancelled(View v)459         void onDragCancelled(View v);
460 
461     }
462 
463     public interface OnItemGestureListener {
onSwipe(View view)464         public void onSwipe(View view);
465 
onTouch()466         public void onTouch();
467 
isSwipeEnabled()468         public boolean isSwipeEnabled();
469     }
470 }
471