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