Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2014 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.recents.views;
     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.annotation.TargetApi;
     25 import android.os.Build;
     26 import android.util.DisplayMetrics;
     27 import android.view.MotionEvent;
     28 import android.view.VelocityTracker;
     29 import android.view.View;
     30 import android.view.animation.LinearInterpolator;
     31 import com.android.systemui.recents.RecentsConfiguration;
     32 
     33 /**
     34  * This class facilitates swipe to dismiss. It defines an interface to be implemented by the
     35  * by the class hosting the views that need to swiped, and, using this interface, handles touch
     36  * events and translates / fades / animates the view as it is dismissed.
     37  */
     38 public class SwipeHelper {
     39     static final String TAG = "SwipeHelper";
     40     private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
     41     private static final boolean CONSTRAIN_SWIPE = true;
     42     private static final boolean FADE_OUT_DURING_SWIPE = true;
     43     private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
     44 
     45     public static final int X = 0;
     46     public static final int Y = 1;
     47 
     48     private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
     49 
     50     private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
     51     private int DEFAULT_ESCAPE_ANIMATION_DURATION = 75; // ms
     52     private int MAX_ESCAPE_ANIMATION_DURATION = 150; // ms
     53     private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
     54     private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 250; // ms
     55 
     56     public static float ALPHA_FADE_START = 0.15f; // fraction of thumbnail width
     57                                                  // where fade starts
     58     static final float ALPHA_FADE_END = 0.65f; // fraction of thumbnail width
     59                                               // beyond which alpha->0
     60     private float mMinAlpha = 0f;
     61 
     62     private float mPagingTouchSlop;
     63     Callback mCallback;
     64     private int mSwipeDirection;
     65     private VelocityTracker mVelocityTracker;
     66 
     67     private float mInitialTouchPos;
     68     private boolean mDragging;
     69 
     70     private View mCurrView;
     71     private boolean mCanCurrViewBeDimissed;
     72     private float mDensityScale;
     73 
     74     public boolean mAllowSwipeTowardsStart = true;
     75     public boolean mAllowSwipeTowardsEnd = true;
     76     private boolean mRtl;
     77 
     78     public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
     79             float pagingTouchSlop) {
     80         mCallback = callback;
     81         mSwipeDirection = swipeDirection;
     82         mVelocityTracker = VelocityTracker.obtain();
     83         mDensityScale = densityScale;
     84         mPagingTouchSlop = pagingTouchSlop;
     85     }
     86 
     87     public void setDensityScale(float densityScale) {
     88         mDensityScale = densityScale;
     89     }
     90 
     91     public void setPagingTouchSlop(float pagingTouchSlop) {
     92         mPagingTouchSlop = pagingTouchSlop;
     93     }
     94 
     95     public void cancelOngoingDrag() {
     96         if (mDragging) {
     97             if (mCurrView != null) {
     98                 mCallback.onDragCancelled(mCurrView);
     99                 setTranslation(mCurrView, 0);
    100                 mCallback.onSnapBackCompleted(mCurrView);
    101                 mCurrView = null;
    102             }
    103             mDragging = false;
    104         }
    105     }
    106 
    107     public void resetTranslation(View v) {
    108         setTranslation(v, 0);
    109     }
    110 
    111     private float getPos(MotionEvent ev) {
    112         return mSwipeDirection == X ? ev.getX() : ev.getY();
    113     }
    114 
    115     private float getTranslation(View v) {
    116         return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
    117     }
    118 
    119     private float getVelocity(VelocityTracker vt) {
    120         return mSwipeDirection == X ? vt.getXVelocity() :
    121                 vt.getYVelocity();
    122     }
    123 
    124     private ObjectAnimator createTranslationAnimation(View v, float newPos) {
    125         ObjectAnimator anim = ObjectAnimator.ofFloat(v,
    126                 mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos);
    127         return anim;
    128     }
    129 
    130     private float getPerpendicularVelocity(VelocityTracker vt) {
    131         return mSwipeDirection == X ? vt.getYVelocity() :
    132                 vt.getXVelocity();
    133     }
    134 
    135     private void setTranslation(View v, float translate) {
    136         if (mSwipeDirection == X) {
    137             v.setTranslationX(translate);
    138         } else {
    139             v.setTranslationY(translate);
    140         }
    141     }
    142 
    143     private float getSize(View v) {
    144         final DisplayMetrics dm = v.getContext().getResources().getDisplayMetrics();
    145         return mSwipeDirection == X ? dm.widthPixels : dm.heightPixels;
    146     }
    147 
    148     public void setMinAlpha(float minAlpha) {
    149         mMinAlpha = minAlpha;
    150     }
    151 
    152     float getAlphaForOffset(View view) {
    153         float viewSize = getSize(view);
    154         final float fadeSize = ALPHA_FADE_END * viewSize;
    155         float result = 1.0f;
    156         float pos = getTranslation(view);
    157         if (pos >= viewSize * ALPHA_FADE_START) {
    158             result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
    159         } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
    160             result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
    161         }
    162         result = Math.min(result, 1.0f);
    163         result = Math.max(result, 0f);
    164         return Math.max(mMinAlpha, result);
    165     }
    166 
    167     /**
    168      * Determines whether the given view has RTL layout.
    169      */
    170     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    171     public static boolean isLayoutRtl(View view) {
    172         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    173             return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection();
    174         } else {
    175             return false;
    176         }
    177     }
    178 
    179     public boolean onInterceptTouchEvent(MotionEvent ev) {
    180         final int action = ev.getAction();
    181 
    182         switch (action) {
    183             case MotionEvent.ACTION_DOWN:
    184                 mDragging = false;
    185                 mCurrView = mCallback.getChildAtPosition(ev);
    186                 mVelocityTracker.clear();
    187                 if (mCurrView != null) {
    188                     mRtl = isLayoutRtl(mCurrView);
    189                     mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
    190                     mVelocityTracker.addMovement(ev);
    191                     mInitialTouchPos = getPos(ev);
    192                 } else {
    193                     mCanCurrViewBeDimissed = false;
    194                 }
    195                 break;
    196             case MotionEvent.ACTION_MOVE:
    197                 if (mCurrView != null) {
    198                     mVelocityTracker.addMovement(ev);
    199                     float pos = getPos(ev);
    200                     float delta = pos - mInitialTouchPos;
    201                     if (Math.abs(delta) > mPagingTouchSlop) {
    202                         mCallback.onBeginDrag(mCurrView);
    203                         mDragging = true;
    204                         mInitialTouchPos = pos - getTranslation(mCurrView);
    205                     }
    206                 }
    207                 break;
    208             case MotionEvent.ACTION_UP:
    209             case MotionEvent.ACTION_CANCEL:
    210                 mDragging = false;
    211                 mCurrView = null;
    212                 break;
    213         }
    214         return mDragging;
    215     }
    216 
    217     /**
    218      * @param view The view to be dismissed
    219      * @param velocity The desired pixels/second speed at which the view should move
    220      */
    221     private void dismissChild(final View view, float velocity) {
    222         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
    223         float newPos;
    224         if (velocity < 0
    225                 || (velocity == 0 && getTranslation(view) < 0)
    226                 // if we use the Menu to dismiss an item in landscape, animate up
    227                 || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) {
    228             newPos = -getSize(view);
    229         } else {
    230             newPos = getSize(view);
    231         }
    232         int duration = MAX_ESCAPE_ANIMATION_DURATION;
    233         if (velocity != 0) {
    234             duration = Math.min(duration,
    235                                 (int) (Math.abs(newPos - getTranslation(view)) *
    236                                         1000f / Math.abs(velocity)));
    237         } else {
    238             duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
    239         }
    240 
    241         ValueAnimator anim = createTranslationAnimation(view, newPos);
    242         anim.setInterpolator(sLinearInterpolator);
    243         anim.setDuration(duration);
    244         anim.addListener(new AnimatorListenerAdapter() {
    245             @Override
    246             public void onAnimationEnd(Animator animation) {
    247                 mCallback.onChildDismissed(view);
    248                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
    249                     view.setAlpha(1.f);
    250                 }
    251             }
    252         });
    253         anim.addUpdateListener(new AnimatorUpdateListener() {
    254             @Override
    255             public void onAnimationUpdate(ValueAnimator animation) {
    256                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
    257                     view.setAlpha(getAlphaForOffset(view));
    258                 }
    259             }
    260         });
    261         anim.start();
    262     }
    263 
    264     private void snapChild(final View view, float velocity) {
    265         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
    266         ValueAnimator anim = createTranslationAnimation(view, 0);
    267         int duration = SNAP_ANIM_LEN;
    268         anim.setDuration(duration);
    269         anim.setInterpolator(RecentsConfiguration.getInstance().linearOutSlowInInterpolator);
    270         anim.addUpdateListener(new AnimatorUpdateListener() {
    271             @Override
    272             public void onAnimationUpdate(ValueAnimator animation) {
    273                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
    274                     view.setAlpha(getAlphaForOffset(view));
    275                 }
    276                 mCallback.onSwipeChanged(mCurrView, view.getTranslationX());
    277             }
    278         });
    279         anim.addListener(new AnimatorListenerAdapter() {
    280             @Override
    281             public void onAnimationEnd(Animator animation) {
    282                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
    283                     view.setAlpha(1.0f);
    284                 }
    285                 mCallback.onSnapBackCompleted(view);
    286             }
    287         });
    288         anim.start();
    289     }
    290 
    291     public boolean onTouchEvent(MotionEvent ev) {
    292         if (!mDragging) {
    293             if (!onInterceptTouchEvent(ev)) {
    294                 return mCanCurrViewBeDimissed;
    295             }
    296         }
    297 
    298         mVelocityTracker.addMovement(ev);
    299         final int action = ev.getAction();
    300         switch (action) {
    301             case MotionEvent.ACTION_OUTSIDE:
    302             case MotionEvent.ACTION_MOVE:
    303                 if (mCurrView != null) {
    304                     float delta = getPos(ev) - mInitialTouchPos;
    305                     setSwipeAmount(delta);
    306                     mCallback.onSwipeChanged(mCurrView, delta);
    307                 }
    308                 break;
    309             case MotionEvent.ACTION_UP:
    310             case MotionEvent.ACTION_CANCEL:
    311                 if (mCurrView != null) {
    312                     endSwipe(mVelocityTracker);
    313                 }
    314                 break;
    315         }
    316         return true;
    317     }
    318 
    319     private void setSwipeAmount(float amount) {
    320         // don't let items that can't be dismissed be dragged more than
    321         // maxScrollDistance
    322         if (CONSTRAIN_SWIPE
    323                 && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) {
    324             float size = getSize(mCurrView);
    325             float maxScrollDistance = 0.15f * size;
    326             if (Math.abs(amount) >= size) {
    327                 amount = amount > 0 ? maxScrollDistance : -maxScrollDistance;
    328             } else {
    329                 amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2));
    330             }
    331         }
    332         setTranslation(mCurrView, amount);
    333         if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
    334             float alpha = getAlphaForOffset(mCurrView);
    335             mCurrView.setAlpha(alpha);
    336         }
    337     }
    338 
    339     private boolean isValidSwipeDirection(float amount) {
    340         if (mSwipeDirection == X) {
    341             if (mRtl) {
    342                 return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart;
    343             } else {
    344                 return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd;
    345             }
    346         }
    347 
    348         // Vertical swipes are always valid.
    349         return true;
    350     }
    351 
    352     private void endSwipe(VelocityTracker velocityTracker) {
    353         float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
    354         velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
    355         float velocity = getVelocity(velocityTracker);
    356         float perpendicularVelocity = getPerpendicularVelocity(velocityTracker);
    357         float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
    358         float translation = getTranslation(mCurrView);
    359         // Decide whether to dismiss the current view
    360         boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
    361                 Math.abs(translation) > 0.6 * getSize(mCurrView);
    362         boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
    363                 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
    364                 (velocity > 0) == (translation > 0);
    365 
    366         boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
    367                 && isValidSwipeDirection(translation)
    368                 && (childSwipedFastEnough || childSwipedFarEnough);
    369 
    370         if (dismissChild) {
    371             // flingadingy
    372             dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
    373         } else {
    374             // snappity
    375             mCallback.onDragCancelled(mCurrView);
    376             snapChild(mCurrView, velocity);
    377         }
    378     }
    379 
    380     public interface Callback {
    381         View getChildAtPosition(MotionEvent ev);
    382 
    383         boolean canChildBeDismissed(View v);
    384 
    385         void onBeginDrag(View v);
    386 
    387         void onSwipeChanged(View v, float delta);
    388 
    389         void onChildDismissed(View v);
    390 
    391         void onSnapBackCompleted(View v);
    392 
    393         void onDragCancelled(View v);
    394     }
    395 }
    396