Home | History | Annotate | Download | only in ui
      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.mail.ui;
     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.view.MotionEvent;
     29 import android.view.VelocityTracker;
     30 import android.view.View;
     31 import android.view.animation.DecelerateInterpolator;
     32 
     33 import com.android.mail.R;
     34 import com.android.mail.utils.LogUtils;
     35 import com.android.mail.utils.Utils;
     36 
     37 public class SwipeHelper {
     38     static final String TAG = "com.android.systemui.SwipeHelper";
     39     private static final boolean DEBUG_INVALIDATE = false;
     40     private static final boolean CONSTRAIN_SWIPE = true;
     41     private static final boolean FADE_OUT_DURING_SWIPE = true;
     42     private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
     43     // Turn on for debugging only during development.
     44     private static final boolean LOG_SWIPE_DISMISS_VELOCITY = false;
     45 
     46     public static final int X = 0;
     47     public static final int Y = 1;
     48 
     49     private static DecelerateInterpolator sDecelerateInterpolator =
     50                                                         new DecelerateInterpolator(1.0f);
     51 
     52     private static int SWIPE_ESCAPE_VELOCITY = -1;
     53     private static int DEFAULT_ESCAPE_ANIMATION_DURATION;
     54     private static int MAX_ESCAPE_ANIMATION_DURATION;
     55     private static int MAX_DISMISS_VELOCITY;
     56     private static int SNAP_ANIM_LEN;
     57     private static float MIN_SWIPE;
     58 
     59     public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
     60                                                  // where fade starts
     61     public static float ALPHA_TEXT_FADE_START = 0.4f;
     62     static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width
     63                                               // beyond which alpha->0
     64     private static final float FACTOR = 1.2f;
     65 
     66     /* Dead region where swipe cannot be initiated. */
     67     private final static int DEAD_REGION_FOR_SWIPE = 56;
     68 
     69     private float mPagingTouchSlop;
     70     private final Callback mCallback;
     71     private final int mSwipeDirection;
     72     private final VelocityTracker mVelocityTracker;
     73 
     74     private float mInitialTouchPosX;
     75     private boolean mDragging;
     76     private SwipeableItemView mCurrView;
     77     private View mCurrAnimView;
     78     private boolean mCanCurrViewBeDimissed;
     79     private float mDensityScale;
     80     private float mLastY;
     81     private float mInitialTouchPosY;
     82     private LeaveBehindItem mPrevView;
     83 
     84     public SwipeHelper(Context context, int swipeDirection, Callback callback, float densityScale,
     85             float pagingTouchSlop) {
     86         mCallback = callback;
     87         mSwipeDirection = swipeDirection;
     88         mVelocityTracker = VelocityTracker.obtain();
     89         mDensityScale = densityScale;
     90         mPagingTouchSlop = pagingTouchSlop;
     91         if (SWIPE_ESCAPE_VELOCITY == -1) {
     92             Resources res = context.getResources();
     93             SWIPE_ESCAPE_VELOCITY = res.getInteger(R.integer.swipe_escape_velocity);
     94             DEFAULT_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.escape_animation_duration);
     95             MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration);
     96             MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity);
     97             SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration);
     98             MIN_SWIPE = res.getDimension(R.dimen.min_swipe);
     99         }
    100     }
    101 
    102     public void setDensityScale(float densityScale) {
    103         mDensityScale = densityScale;
    104     }
    105 
    106     public void setPagingTouchSlop(float pagingTouchSlop) {
    107         mPagingTouchSlop = pagingTouchSlop;
    108     }
    109 
    110     private float getVelocity(VelocityTracker vt) {
    111         return mSwipeDirection == X ? vt.getXVelocity() :
    112                 vt.getYVelocity();
    113     }
    114 
    115     private ObjectAnimator createTranslationAnimation(View v, float newPos) {
    116         ObjectAnimator anim = ObjectAnimator.ofFloat(v,
    117                 mSwipeDirection == X ? "translationX" : "translationY", newPos);
    118         return anim;
    119     }
    120 
    121     private ObjectAnimator createDismissAnimation(View v, float newPos, int duration) {
    122         ObjectAnimator anim = createTranslationAnimation(v, newPos);
    123         anim.setInterpolator(sDecelerateInterpolator);
    124         anim.setDuration(duration);
    125         return anim;
    126     }
    127 
    128     private float getPerpendicularVelocity(VelocityTracker vt) {
    129         return mSwipeDirection == X ? vt.getYVelocity() :
    130                 vt.getXVelocity();
    131     }
    132 
    133     private void setTranslation(View v, float translate) {
    134         if (mSwipeDirection == X) {
    135             v.setTranslationX(translate);
    136         } else {
    137             v.setTranslationY(translate);
    138         }
    139     }
    140 
    141     private float getSize(View v) {
    142         return mSwipeDirection == X ? v.getMeasuredWidth() :
    143                 v.getMeasuredHeight();
    144     }
    145 
    146     private float getAlphaForOffset(View view) {
    147         float viewSize = getSize(view);
    148         final float fadeSize = ALPHA_FADE_END * viewSize;
    149         float result = 1.0f;
    150         float pos = view.getTranslationX();
    151         if (pos >= viewSize * ALPHA_FADE_START) {
    152             result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
    153         } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
    154             result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
    155         }
    156         float minAlpha = 0.5f;
    157         return Math.max(minAlpha, result);
    158     }
    159 
    160     private float getTextAlphaForOffset(View view) {
    161         float viewSize = getSize(view);
    162         final float fadeSize = ALPHA_TEXT_FADE_START * viewSize;
    163         float result = 1.0f;
    164         float pos = view.getTranslationX();
    165         if (pos >= 0) {
    166             result = 1.0f - pos / fadeSize;
    167         } else if (pos < 0) {
    168             result = 1.0f + pos / fadeSize;
    169         }
    170         return Math.max(0, result);
    171     }
    172 
    173     // invalidate the view's own bounds all the way up the view hierarchy
    174     public static void invalidateGlobalRegion(View view) {
    175         invalidateGlobalRegion(
    176             view,
    177             new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
    178     }
    179 
    180     // invalidate a rectangle relative to the view's coordinate system all the way up the view
    181     // hierarchy
    182     public static void invalidateGlobalRegion(View view, RectF childBounds) {
    183         //childBounds.offset(view.getTranslationX(), view.getTranslationY());
    184         if (DEBUG_INVALIDATE)
    185             LogUtils.v(TAG, "-------------");
    186         while (view.getParent() != null && view.getParent() instanceof View) {
    187             view = (View) view.getParent();
    188             view.getMatrix().mapRect(childBounds);
    189             view.invalidate((int) Math.floor(childBounds.left),
    190                             (int) Math.floor(childBounds.top),
    191                             (int) Math.ceil(childBounds.right),
    192                             (int) Math.ceil(childBounds.bottom));
    193             if (DEBUG_INVALIDATE) {
    194                 LogUtils.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
    195                         + "," + (int) Math.floor(childBounds.top)
    196                         + "," + (int) Math.ceil(childBounds.right)
    197                         + "," + (int) Math.ceil(childBounds.bottom));
    198             }
    199         }
    200     }
    201 
    202     public boolean onInterceptTouchEvent(MotionEvent ev) {
    203         final int action = ev.getAction();
    204         switch (action) {
    205             case MotionEvent.ACTION_DOWN:
    206                 mLastY = ev.getY();
    207                 mDragging = false;
    208                 View view = mCallback.getChildAtPosition(ev);
    209                 if (view instanceof SwipeableItemView) {
    210                     mCurrView = (SwipeableItemView) view;
    211                 }
    212                 mVelocityTracker.clear();
    213                 if (mCurrView != null) {
    214                     mCurrAnimView = mCurrView.getSwipeableView().getView();
    215                     mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
    216                     mVelocityTracker.addMovement(ev);
    217                     mInitialTouchPosX = ev.getX();
    218                     mInitialTouchPosY = ev.getY();
    219                 }
    220                 mCallback.cancelDismissCounter();
    221                 break;
    222             case MotionEvent.ACTION_MOVE:
    223                 if (mCurrView != null) {
    224                     // Check the movement direction.
    225                     if (mLastY >= 0 && !mDragging) {
    226                         float currY = ev.getY();
    227                         float currX = ev.getX();
    228                         float deltaY = Math.abs(currY - mInitialTouchPosY);
    229                         float deltaX = Math.abs(currX - mInitialTouchPosX);
    230                         if (deltaY > mCurrView.getMinAllowScrollDistance()
    231                                 && deltaY > (FACTOR * deltaX)) {
    232                             mLastY = ev.getY();
    233                             mCallback.onScroll();
    234                             return false;
    235                         }
    236                     }
    237                     mVelocityTracker.addMovement(ev);
    238                     float pos = ev.getX();
    239                     float delta = pos - mInitialTouchPosX;
    240                     if (Math.abs(delta) > mPagingTouchSlop) {
    241                         mCallback.onBeginDrag(mCurrView.getSwipeableView().getView());
    242                         mPrevView = mCallback.getLastSwipedItem();
    243                         mDragging = true;
    244                         mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX();
    245                         mInitialTouchPosY = ev.getY();
    246                     }
    247                 }
    248                 mLastY = ev.getY();
    249                 break;
    250             case MotionEvent.ACTION_UP:
    251             case MotionEvent.ACTION_CANCEL:
    252                 mDragging = false;
    253                 mCurrView = null;
    254                 mCurrAnimView = null;
    255                 mLastY = -1;
    256                 break;
    257         }
    258         return mDragging;
    259     }
    260 
    261     /**
    262      * @param view The view to be dismissed
    263      * @param velocity The desired pixels/second speed at which the view should
    264      *            move
    265      */
    266     private void dismissChild(final SwipeableItemView view, float velocity) {
    267         final View animView = mCurrView.getSwipeableView().getView();
    268         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
    269         float newPos = determinePos(animView, velocity);
    270         int duration = determineDuration(animView, newPos, velocity);
    271 
    272         Utils.enableHardwareLayer(animView);
    273         ObjectAnimator anim = createDismissAnimation(animView, newPos, duration);
    274         anim.addListener(new AnimatorListenerAdapter() {
    275             @Override
    276             public void onAnimationEnd(Animator animation) {
    277                 mCallback.onChildDismissed(mCurrView);
    278                 animView.setLayerType(View.LAYER_TYPE_NONE, null);
    279             }
    280         });
    281         anim.addUpdateListener(new AnimatorUpdateListener() {
    282             @Override
    283             public void onAnimationUpdate(ValueAnimator animation) {
    284                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
    285                     animView.setAlpha(getAlphaForOffset(animView));
    286                 }
    287                 invalidateGlobalRegion(animView);
    288             }
    289         });
    290         anim.start();
    291     }
    292 
    293     private static int determineDuration(View animView, float newPos, float velocity) {
    294         int duration = MAX_ESCAPE_ANIMATION_DURATION;
    295         if (velocity != 0) {
    296             duration = Math
    297                     .min(duration,
    298                             (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math
    299                                     .abs(velocity)));
    300         } else {
    301             duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
    302         }
    303         return duration;
    304     }
    305 
    306     private float determinePos(View animView, float velocity) {
    307         final float newPos;
    308         if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0)
    309         // if we use the Menu to dismiss an item in landscape, animate up
    310                 || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) {
    311             newPos = -getSize(animView);
    312         } else {
    313             newPos = getSize(animView);
    314         }
    315         return newPos;
    316     }
    317 
    318     public void snapChild(final SwipeableItemView view) {
    319         final View animView = view.getSwipeableView().getView();
    320         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
    321         final ObjectAnimator anim = createTranslationAnimation(animView, 0);
    322         final int duration = SNAP_ANIM_LEN;
    323         anim.setDuration(duration);
    324         anim.addUpdateListener(new AnimatorUpdateListener() {
    325             @Override
    326             public void onAnimationUpdate(ValueAnimator animation) {
    327                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
    328                     animView.setAlpha(getAlphaForOffset(animView));
    329                 }
    330                 invalidateGlobalRegion(animView);
    331             }
    332         });
    333         anim.addListener(new Animator.AnimatorListener() {
    334             @Override
    335             public void onAnimationStart(Animator animation) {
    336             }
    337             @Override
    338             public void onAnimationEnd(Animator animation) {
    339                 animView.setAlpha(1.0f);
    340                 mCallback.onDragCancelled(mCurrView);
    341             }
    342             @Override
    343             public void onAnimationCancel(Animator animation) {
    344             }
    345             @Override
    346             public void onAnimationRepeat(Animator animation) {
    347             }
    348         });
    349         anim.start();
    350     }
    351 
    352     public boolean onTouchEvent(MotionEvent ev) {
    353         if (!mDragging) {
    354             return false;
    355         }
    356         mVelocityTracker.addMovement(ev);
    357         final int action = ev.getAction();
    358         switch (action) {
    359             case MotionEvent.ACTION_OUTSIDE:
    360             case MotionEvent.ACTION_MOVE:
    361                 if (mCurrView != null) {
    362                     float deltaX = ev.getX() - mInitialTouchPosX;
    363                     // If the swipe started in the dead region, ignore it.
    364                     if (mInitialTouchPosX <= (DEAD_REGION_FOR_SWIPE * mDensityScale)){
    365                             return true;
    366                     }
    367                     // If the user has gone vertical and not gone horizontalish AT
    368                     // LEAST minBeforeLock, switch to scroll. Otherwise, cancel
    369                     // the swipe.
    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                         if (mPrevView != null) {
    392                             // Base how much the text of the prev item is faded
    393                             // on how far the current item has moved.
    394                             mPrevView.setTextAlpha(getTextAlphaForOffset(mCurrAnimView));
    395                         }
    396                     }
    397                     invalidateGlobalRegion(mCurrView.getSwipeableView().getView());
    398                 }
    399                 break;
    400             case MotionEvent.ACTION_UP:
    401             case MotionEvent.ACTION_CANCEL:
    402                 if (mCurrView != null) {
    403                     float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
    404                     mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
    405                     float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
    406                     float velocity = getVelocity(mVelocityTracker);
    407                     float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
    408 
    409                     // Decide whether to dismiss the current view
    410                     // Tweak constants below as required to prevent erroneous
    411                     // swipe/dismiss
    412                     float translation = Math.abs(mCurrAnimView.getTranslationX());
    413                     float currAnimViewSize = getSize(mCurrAnimView);
    414                     // Long swipe = translation of .4 * width
    415                     boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH
    416                             && translation > 0.4 * currAnimViewSize;
    417                     // Fast swipe = > escapeVelocity and translation of .1 *
    418                     // width
    419                     boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity)
    420                             && (Math.abs(velocity) > Math.abs(perpendicularVelocity))
    421                             && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0)
    422                             && translation > 0.05 * currAnimViewSize;
    423                     if (LOG_SWIPE_DISMISS_VELOCITY) {
    424                         LogUtils.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/"
    425                                 + perpendicularVelocity + ", x: " + translation + "/"
    426                                 + currAnimViewSize);
    427                     }
    428 
    429                     boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
    430                             && (childSwipedFastEnough || childSwipedFarEnough);
    431 
    432                     if (dismissChild) {
    433                         dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
    434                     } else {
    435                         snapChild(mCurrView);
    436                     }
    437                 }
    438                 break;
    439         }
    440         return true;
    441     }
    442 
    443     public interface Callback {
    444         View getChildAtPosition(MotionEvent ev);
    445 
    446         void cancelDismissCounter();
    447 
    448         void onScroll();
    449 
    450         boolean canChildBeDismissed(SwipeableItemView v);
    451 
    452         void onBeginDrag(View v);
    453 
    454         void onChildDismissed(SwipeableItemView v);
    455 
    456         void onDragCancelled(SwipeableItemView v);
    457 
    458         ConversationSelectionSet getSelectionSet();
    459 
    460         LeaveBehindItem getLastSwipedItem();
    461     }
    462 }
    463