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.graphics.RectF;
     25 import android.os.Handler;
     26 import android.util.Log;
     27 import android.view.MotionEvent;
     28 import android.view.VelocityTracker;
     29 import android.view.View;
     30 import android.view.ViewConfiguration;
     31 import android.view.accessibility.AccessibilityEvent;
     32 import android.view.animation.LinearInterpolator;
     33 
     34 public class SwipeHelper implements Gefingerpoken {
     35     static final String TAG = "com.android.systemui.SwipeHelper";
     36     private static final boolean DEBUG = false;
     37     private static final boolean DEBUG_INVALIDATE = false;
     38     private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
     39     private static final boolean CONSTRAIN_SWIPE = true;
     40     private static final boolean FADE_OUT_DURING_SWIPE = true;
     41     private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
     42 
     43     public static final int X = 0;
     44     public static final int Y = 1;
     45 
     46     private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
     47 
     48     private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
     49     private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
     50     private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
     51     private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
     52     private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
     53 
     54     public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
     55                                                  // where fade starts
     56     static final float ALPHA_FADE_END = 0.5f; // fraction of thumbnail width
     57                                               // beyond which alpha->0
     58     private float mMinAlpha = 0f;
     59 
     60     private float mPagingTouchSlop;
     61     private Callback mCallback;
     62     private Handler mHandler;
     63     private int mSwipeDirection;
     64     private VelocityTracker mVelocityTracker;
     65 
     66     private float mInitialTouchPos;
     67     private boolean mDragging;
     68     private View mCurrView;
     69     private View mCurrAnimView;
     70     private boolean mCanCurrViewBeDimissed;
     71     private float mDensityScale;
     72 
     73     private boolean mLongPressSent;
     74     private View.OnLongClickListener mLongPressListener;
     75     private Runnable mWatchLongPress;
     76     private long mLongPressTimeout;
     77 
     78     public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
     79             float pagingTouchSlop) {
     80         mCallback = callback;
     81         mHandler = new Handler();
     82         mSwipeDirection = swipeDirection;
     83         mVelocityTracker = VelocityTracker.obtain();
     84         mDensityScale = densityScale;
     85         mPagingTouchSlop = pagingTouchSlop;
     86 
     87         mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press!
     88     }
     89 
     90     public void setLongPressListener(View.OnLongClickListener listener) {
     91         mLongPressListener = listener;
     92     }
     93 
     94     public void setDensityScale(float densityScale) {
     95         mDensityScale = densityScale;
     96     }
     97 
     98     public void setPagingTouchSlop(float pagingTouchSlop) {
     99         mPagingTouchSlop = pagingTouchSlop;
    100     }
    101 
    102     private float getPos(MotionEvent ev) {
    103         return mSwipeDirection == X ? ev.getX() : ev.getY();
    104     }
    105 
    106     private float getTranslation(View v) {
    107         return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
    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 float getPerpendicularVelocity(VelocityTracker vt) {
    122         return mSwipeDirection == X ? vt.getYVelocity() :
    123                 vt.getXVelocity();
    124     }
    125 
    126     private void setTranslation(View v, float translate) {
    127         if (mSwipeDirection == X) {
    128             v.setTranslationX(translate);
    129         } else {
    130             v.setTranslationY(translate);
    131         }
    132     }
    133 
    134     private float getSize(View v) {
    135         return mSwipeDirection == X ? v.getMeasuredWidth() :
    136                 v.getMeasuredHeight();
    137     }
    138 
    139     public void setMinAlpha(float minAlpha) {
    140         mMinAlpha = minAlpha;
    141     }
    142 
    143     private float getAlphaForOffset(View view) {
    144         float viewSize = getSize(view);
    145         final float fadeSize = ALPHA_FADE_END * viewSize;
    146         float result = 1.0f;
    147         float pos = getTranslation(view);
    148         if (pos >= viewSize * ALPHA_FADE_START) {
    149             result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
    150         } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
    151             result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
    152         }
    153         return Math.max(mMinAlpha, result);
    154     }
    155 
    156     private void updateAlphaFromOffset(View animView, boolean dismissable) {
    157         if (FADE_OUT_DURING_SWIPE && dismissable) {
    158             float alpha = getAlphaForOffset(animView);
    159             if (alpha != 0f && alpha != 1f) {
    160                 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    161             } else {
    162                 animView.setLayerType(View.LAYER_TYPE_NONE, null);
    163             }
    164             animView.setAlpha(getAlphaForOffset(animView));
    165         }
    166         invalidateGlobalRegion(animView);
    167     }
    168 
    169     // invalidate the view's own bounds all the way up the view hierarchy
    170     public static void invalidateGlobalRegion(View view) {
    171         invalidateGlobalRegion(
    172             view,
    173             new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
    174     }
    175 
    176     // invalidate a rectangle relative to the view's coordinate system all the way up the view
    177     // hierarchy
    178     public static void invalidateGlobalRegion(View view, RectF childBounds) {
    179         //childBounds.offset(view.getTranslationX(), view.getTranslationY());
    180         if (DEBUG_INVALIDATE)
    181             Log.v(TAG, "-------------");
    182         while (view.getParent() != null && view.getParent() instanceof View) {
    183             view = (View) view.getParent();
    184             view.getMatrix().mapRect(childBounds);
    185             view.invalidate((int) Math.floor(childBounds.left),
    186                             (int) Math.floor(childBounds.top),
    187                             (int) Math.ceil(childBounds.right),
    188                             (int) Math.ceil(childBounds.bottom));
    189             if (DEBUG_INVALIDATE) {
    190                 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
    191                         + "," + (int) Math.floor(childBounds.top)
    192                         + "," + (int) Math.ceil(childBounds.right)
    193                         + "," + (int) Math.ceil(childBounds.bottom));
    194             }
    195         }
    196     }
    197 
    198     public void removeLongPressCallback() {
    199         if (mWatchLongPress != null) {
    200             mHandler.removeCallbacks(mWatchLongPress);
    201             mWatchLongPress = null;
    202         }
    203     }
    204 
    205     public boolean onInterceptTouchEvent(MotionEvent ev) {
    206         final int action = ev.getAction();
    207 
    208         switch (action) {
    209             case MotionEvent.ACTION_DOWN:
    210                 mDragging = false;
    211                 mLongPressSent = false;
    212                 mCurrView = mCallback.getChildAtPosition(ev);
    213                 mVelocityTracker.clear();
    214                 if (mCurrView != null) {
    215                     mCurrAnimView = mCallback.getChildContentView(mCurrView);
    216                     mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
    217                     mVelocityTracker.addMovement(ev);
    218                     mInitialTouchPos = getPos(ev);
    219 
    220                     if (mLongPressListener != null) {
    221                         if (mWatchLongPress == null) {
    222                             mWatchLongPress = new Runnable() {
    223                                 @Override
    224                                 public void run() {
    225                                     if (mCurrView != null && !mLongPressSent) {
    226                                         mLongPressSent = true;
    227                                         mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
    228                                         mLongPressListener.onLongClick(mCurrView);
    229                                     }
    230                                 }
    231                             };
    232                         }
    233                         mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
    234                     }
    235 
    236                 }
    237                 break;
    238 
    239             case MotionEvent.ACTION_MOVE:
    240                 if (mCurrView != null && !mLongPressSent) {
    241                     mVelocityTracker.addMovement(ev);
    242                     float pos = getPos(ev);
    243                     float delta = pos - mInitialTouchPos;
    244                     if (Math.abs(delta) > mPagingTouchSlop) {
    245                         mCallback.onBeginDrag(mCurrView);
    246                         mDragging = true;
    247                         mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
    248 
    249                         removeLongPressCallback();
    250                     }
    251                 }
    252 
    253                 break;
    254 
    255             case MotionEvent.ACTION_UP:
    256             case MotionEvent.ACTION_CANCEL:
    257                 mDragging = false;
    258                 mCurrView = null;
    259                 mCurrAnimView = null;
    260                 mLongPressSent = false;
    261                 removeLongPressCallback();
    262                 break;
    263         }
    264         return mDragging;
    265     }
    266 
    267     /**
    268      * @param view The view to be dismissed
    269      * @param velocity The desired pixels/second speed at which the view should move
    270      */
    271     public void dismissChild(final View view, float velocity) {
    272         final View animView = mCallback.getChildContentView(view);
    273         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
    274         float newPos;
    275 
    276         if (velocity < 0
    277                 || (velocity == 0 && getTranslation(animView) < 0)
    278                 // if we use the Menu to dismiss an item in landscape, animate up
    279                 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
    280             newPos = -getSize(animView);
    281         } else {
    282             newPos = getSize(animView);
    283         }
    284         int duration = MAX_ESCAPE_ANIMATION_DURATION;
    285         if (velocity != 0) {
    286             duration = Math.min(duration,
    287                                 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
    288                                         .abs(velocity)));
    289         } else {
    290             duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
    291         }
    292 
    293         animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    294         ObjectAnimator anim = createTranslationAnimation(animView, newPos);
    295         anim.setInterpolator(sLinearInterpolator);
    296         anim.setDuration(duration);
    297         anim.addListener(new AnimatorListenerAdapter() {
    298             public void onAnimationEnd(Animator animation) {
    299                 mCallback.onChildDismissed(view);
    300                 animView.setLayerType(View.LAYER_TYPE_NONE, null);
    301             }
    302         });
    303         anim.addUpdateListener(new AnimatorUpdateListener() {
    304             public void onAnimationUpdate(ValueAnimator animation) {
    305                 updateAlphaFromOffset(animView, canAnimViewBeDismissed);
    306             }
    307         });
    308         anim.start();
    309     }
    310 
    311     public void snapChild(final View view, float velocity) {
    312         final View animView = mCallback.getChildContentView(view);
    313         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
    314         ObjectAnimator anim = createTranslationAnimation(animView, 0);
    315         int duration = SNAP_ANIM_LEN;
    316         anim.setDuration(duration);
    317         anim.addUpdateListener(new AnimatorUpdateListener() {
    318             public void onAnimationUpdate(ValueAnimator animation) {
    319                 updateAlphaFromOffset(animView, canAnimViewBeDismissed);
    320             }
    321         });
    322         anim.addListener(new AnimatorListenerAdapter() {
    323             public void onAnimationEnd(Animator animator) {
    324                 updateAlphaFromOffset(animView, canAnimViewBeDismissed);
    325             }
    326         });
    327         anim.start();
    328     }
    329 
    330     public boolean onTouchEvent(MotionEvent ev) {
    331         if (mLongPressSent) {
    332             return true;
    333         }
    334 
    335         if (!mDragging) {
    336             // We are not doing anything, make sure the long press callback
    337             // is not still ticking like a bomb waiting to go off.
    338             removeLongPressCallback();
    339             return false;
    340         }
    341 
    342         mVelocityTracker.addMovement(ev);
    343         final int action = ev.getAction();
    344         switch (action) {
    345             case MotionEvent.ACTION_OUTSIDE:
    346             case MotionEvent.ACTION_MOVE:
    347                 if (mCurrView != null) {
    348                     float delta = getPos(ev) - mInitialTouchPos;
    349                     // don't let items that can't be dismissed be dragged more than
    350                     // maxScrollDistance
    351                     if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
    352                         float size = getSize(mCurrAnimView);
    353                         float maxScrollDistance = 0.15f * size;
    354                         if (Math.abs(delta) >= size) {
    355                             delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
    356                         } else {
    357                             delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
    358                         }
    359                     }
    360                     setTranslation(mCurrAnimView, delta);
    361 
    362                     updateAlphaFromOffset(mCurrAnimView, mCanCurrViewBeDimissed);
    363                 }
    364                 break;
    365             case MotionEvent.ACTION_UP:
    366             case MotionEvent.ACTION_CANCEL:
    367                 if (mCurrView != null) {
    368                     float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
    369                     mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
    370                     float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
    371                     float velocity = getVelocity(mVelocityTracker);
    372                     float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
    373 
    374                     // Decide whether to dismiss the current view
    375                     boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
    376                             Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
    377                     boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
    378                             (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
    379                             (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
    380 
    381                     boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
    382                             (childSwipedFastEnough || childSwipedFarEnough);
    383 
    384                     if (dismissChild) {
    385                         // flingadingy
    386                         dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
    387                     } else {
    388                         // snappity
    389                         mCallback.onDragCancelled(mCurrView);
    390                         snapChild(mCurrView, velocity);
    391                     }
    392                 }
    393                 break;
    394         }
    395         return true;
    396     }
    397 
    398     public interface Callback {
    399         View getChildAtPosition(MotionEvent ev);
    400 
    401         View getChildContentView(View v);
    402 
    403         boolean canChildBeDismissed(View v);
    404 
    405         void onBeginDrag(View v);
    406 
    407         void onChildDismissed(View v);
    408 
    409         void onDragCancelled(View v);
    410     }
    411 }
    412