Home | History | Annotate | Download | only in list
      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 
     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 
    116     public void setDensityScale(float densityScale) {
    117         mDensityScale = densityScale;
    118     }
    119 
    120     public void setPagingTouchSlop(float pagingTouchSlop) {
    121         mPagingTouchSlop = pagingTouchSlop;
    122     }
    123 
    124     public void setChildSwipedFarEnoughFactor(float factor) {
    125         mChildSwipedFarEnoughFactor = factor;
    126     }
    127 
    128     public void setChildSwipedFastEnoughFactor(float factor) {
    129         mChildSwipedFastEnoughFactor = factor;
    130     }
    131 
    132     private float getVelocity(VelocityTracker vt) {
    133         return mSwipeDirection == X ? vt.getXVelocity() :
    134                 vt.getYVelocity();
    135     }
    136 
    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 
    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 
    150     private float getPerpendicularVelocity(VelocityTracker vt) {
    151         return mSwipeDirection == X ? vt.getYVelocity() :
    152                 vt.getXVelocity();
    153     }
    154 
    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 
    163     private float getSize(View v) {
    164         return mSwipeDirection == X ? v.getMeasuredWidth() :
    165                 v.getMeasuredHeight();
    166     }
    167 
    168     public void setMinAlpha(float minAlpha) {
    169         mMinAlpha = minAlpha;
    170     }
    171 
    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
    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
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    438     public static void setSwipeable(View view, boolean swipeable) {
    439         view.setTag(IS_SWIPEABLE_TAG, swipeable ? IS_SWIPEABLE : null);
    440     }
    441 
    442     public static boolean isSwipeable(View view) {
    443         return IS_SWIPEABLE == view.getTag(IS_SWIPEABLE_TAG);
    444     }
    445 
    446     public interface SwipeHelperCallback {
    447         View getChildAtPosition(MotionEvent ev);
    448 
    449         View getChildContentView(View v);
    450 
    451         void onScroll();
    452 
    453         boolean canChildBeDismissed(View v);
    454 
    455         void onBeginDrag(View v);
    456 
    457         void onChildDismissed(View v);
    458 
    459         void onDragCancelled(View v);
    460 
    461     }
    462 
    463     public interface OnItemGestureListener {
    464         public void onSwipe(View view);
    465 
    466         public void onTouch();
    467 
    468         public boolean isSwipeEnabled();
    469     }
    470 }
    471