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