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