Home | History | Annotate | Download | only in allapps
      1 package com.android.launcher3.allapps;
      2 
      3 import android.content.Context;
      4 import android.util.Log;
      5 import android.view.MotionEvent;
      6 import android.view.ViewConfiguration;
      7 import android.view.animation.Interpolator;
      8 
      9 /**
     10  * One dimensional scroll gesture detector for all apps container pull up interaction.
     11  * Client (e.g., AllAppsTransitionController) of this class can register a listener.
     12  * <p/>
     13  * Features that this gesture detector can support.
     14  */
     15 public class VerticalPullDetector {
     16 
     17     private static final boolean DBG = false;
     18     private static final String TAG = "VerticalPullDetector";
     19 
     20     private float mTouchSlop;
     21 
     22     private int mScrollConditions;
     23     public static final int DIRECTION_UP = 1 << 0;
     24     public static final int DIRECTION_DOWN = 1 << 1;
     25     public static final int DIRECTION_BOTH = DIRECTION_DOWN | DIRECTION_UP;
     26 
     27     private static final float ANIMATION_DURATION = 1200;
     28     private static final float FAST_FLING_PX_MS = 10;
     29 
     30     /**
     31      * The minimum release velocity in pixels per millisecond that triggers fling..
     32      */
     33     public static final float RELEASE_VELOCITY_PX_MS = 1.0f;
     34 
     35     /**
     36      * The time constant used to calculate dampening in the low-pass filter of scroll velocity.
     37      * Cutoff frequency is set at 10 Hz.
     38      */
     39     public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10);
     40 
     41     /* Scroll state, this is set to true during dragging and animation. */
     42     private ScrollState mState = ScrollState.IDLE;
     43 
     44     enum ScrollState {
     45         IDLE,
     46         DRAGGING,      // onDragStart, onDrag
     47         SETTLING       // onDragEnd
     48     }
     49 
     50     ;
     51 
     52     //------------------- ScrollState transition diagram -----------------------------------
     53     //
     54     // IDLE ->      (mDisplacement > mTouchSlop) -> DRAGGING
     55     // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
     56     // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
     57     // SETTLING -> (View settled) -> IDLE
     58 
     59     private void setState(ScrollState newState) {
     60         if (DBG) {
     61             Log.d(TAG, "setState:" + mState + "->" + newState);
     62         }
     63         // onDragStart and onDragEnd is reported ONLY on state transition
     64         if (newState == ScrollState.DRAGGING) {
     65             initializeDragging();
     66             if (mState == ScrollState.IDLE) {
     67                 reportDragStart(false /* recatch */);
     68             } else if (mState == ScrollState.SETTLING) {
     69                 reportDragStart(true /* recatch */);
     70             }
     71         }
     72         if (newState == ScrollState.SETTLING) {
     73             reportDragEnd();
     74         }
     75 
     76         mState = newState;
     77     }
     78 
     79     public boolean isDraggingOrSettling() {
     80         return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
     81     }
     82 
     83     /**
     84      * There's no touch and there's no animation.
     85      */
     86     public boolean isIdleState() {
     87         return mState == ScrollState.IDLE;
     88     }
     89 
     90     public boolean isSettlingState() {
     91         return mState == ScrollState.SETTLING;
     92     }
     93 
     94     public boolean isDraggingState() {
     95         return mState == ScrollState.DRAGGING;
     96     }
     97 
     98     private float mDownX;
     99     private float mDownY;
    100 
    101     private float mLastY;
    102     private long mCurrentMillis;
    103 
    104     private float mVelocity;
    105     private float mLastDisplacement;
    106     private float mDisplacementY;
    107     private float mDisplacementX;
    108 
    109     private float mSubtractDisplacement;
    110     private boolean mIgnoreSlopWhenSettling;
    111 
    112     /* Client of this gesture detector can register a callback. */
    113     Listener mListener;
    114 
    115     public void setListener(Listener l) {
    116         mListener = l;
    117     }
    118 
    119     public interface Listener {
    120         void onDragStart(boolean start);
    121 
    122         boolean onDrag(float displacement, float velocity);
    123 
    124         void onDragEnd(float velocity, boolean fling);
    125     }
    126 
    127     public VerticalPullDetector(Context context) {
    128         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    129     }
    130 
    131     public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
    132         mScrollConditions = scrollDirectionFlags;
    133         mIgnoreSlopWhenSettling = ignoreSlop;
    134     }
    135 
    136     private boolean shouldScrollStart() {
    137         // reject cases where the slop condition is not met.
    138         if (Math.abs(mDisplacementY) < mTouchSlop) {
    139             return false;
    140         }
    141 
    142         // reject cases where the angle condition is not met.
    143         float deltaY = Math.abs(mDisplacementY);
    144         float deltaX = Math.max(Math.abs(mDisplacementX), 1);
    145         if (deltaX > deltaY) {
    146             return false;
    147         }
    148         // Check if the client is interested in scroll in current direction.
    149         if (((mScrollConditions & DIRECTION_DOWN) > 0 && mDisplacementY > 0) ||
    150                 ((mScrollConditions & DIRECTION_UP) > 0 && mDisplacementY < 0)) {
    151             return true;
    152         }
    153         return false;
    154     }
    155 
    156     public boolean onTouchEvent(MotionEvent ev) {
    157         switch (ev.getAction()) {
    158             case MotionEvent.ACTION_DOWN:
    159                 mDownX = ev.getX();
    160                 mDownY = ev.getY();
    161                 mLastDisplacement = 0;
    162                 mDisplacementY = 0;
    163                 mVelocity = 0;
    164 
    165                 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
    166                     setState(ScrollState.DRAGGING);
    167                 }
    168                 break;
    169             case MotionEvent.ACTION_MOVE:
    170                 mDisplacementX = ev.getX() - mDownX;
    171                 mDisplacementY = ev.getY() - mDownY;
    172                 computeVelocity(ev);
    173 
    174                 // handle state and listener calls.
    175                 if (mState != ScrollState.DRAGGING && shouldScrollStart()) {
    176                     setState(ScrollState.DRAGGING);
    177                 }
    178                 if (mState == ScrollState.DRAGGING) {
    179                     reportDragging();
    180                 }
    181                 break;
    182             case MotionEvent.ACTION_CANCEL:
    183             case MotionEvent.ACTION_UP:
    184                 // These are synthetic events and there is no need to update internal values.
    185                 if (mState == ScrollState.DRAGGING) {
    186                     setState(ScrollState.SETTLING);
    187                 }
    188                 break;
    189             default:
    190                 //TODO: add multi finger tracking by tracking active pointer.
    191                 break;
    192         }
    193         // Do house keeping.
    194         mLastDisplacement = mDisplacementY;
    195         mLastY = ev.getY();
    196         return true;
    197     }
    198 
    199     public void finishedScrolling() {
    200         setState(ScrollState.IDLE);
    201     }
    202 
    203     private boolean reportDragStart(boolean recatch) {
    204         mListener.onDragStart(!recatch);
    205         if (DBG) {
    206             Log.d(TAG, "onDragStart recatch:" + recatch);
    207         }
    208         return true;
    209     }
    210 
    211     private void initializeDragging() {
    212         if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
    213             mSubtractDisplacement = 0;
    214         }
    215         if (mDisplacementY > 0) {
    216             mSubtractDisplacement = mTouchSlop;
    217         } else {
    218             mSubtractDisplacement = -mTouchSlop;
    219         }
    220     }
    221 
    222     private boolean reportDragging() {
    223         float delta = mDisplacementY - mLastDisplacement;
    224         if (delta != 0) {
    225             if (DBG) {
    226                 Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f",
    227                         mDisplacementY, mVelocity));
    228             }
    229 
    230             return mListener.onDrag(mDisplacementY - mSubtractDisplacement, mVelocity);
    231         }
    232         return true;
    233     }
    234 
    235     private void reportDragEnd() {
    236         if (DBG) {
    237             Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
    238                     mDisplacementY, mVelocity));
    239         }
    240         mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS);
    241 
    242     }
    243 
    244     /**
    245      * Computes the damped velocity using the two motion events and the previous velocity.
    246      */
    247     private float computeVelocity(MotionEvent to) {
    248         return computeVelocity(to.getY() - mLastY, to.getEventTime());
    249     }
    250 
    251     public float computeVelocity(float delta, long currentMillis) {
    252         long previousMillis = mCurrentMillis;
    253         mCurrentMillis = currentMillis;
    254 
    255         float deltaTimeMillis = mCurrentMillis - previousMillis;
    256         float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0;
    257         if (Math.abs(mVelocity) < 0.001f) {
    258             mVelocity = velocity;
    259         } else {
    260             float alpha = computeDampeningFactor(deltaTimeMillis);
    261             mVelocity = interpolate(mVelocity, velocity, alpha);
    262         }
    263         return mVelocity;
    264     }
    265 
    266     /**
    267      * Returns a time-dependent dampening factor using delta time.
    268      */
    269     private static float computeDampeningFactor(float deltaTime) {
    270         return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime);
    271     }
    272 
    273     /**
    274      * Returns the linear interpolation between two values
    275      */
    276     private static float interpolate(float from, float to, float alpha) {
    277         return (1.0f - alpha) * from + alpha * to;
    278     }
    279 
    280     public long calculateDuration(float velocity, float progressNeeded) {
    281         // TODO: make these values constants after tuning.
    282         float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
    283         float travelDistance = Math.max(0.2f, progressNeeded);
    284         long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
    285         if (DBG) {
    286             Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
    287         }
    288         return duration;
    289     }
    290 
    291     public static class ScrollInterpolator implements Interpolator {
    292 
    293         boolean mSteeper;
    294 
    295         public void setVelocityAtZero(float velocity) {
    296             mSteeper = velocity > FAST_FLING_PX_MS;
    297         }
    298 
    299         public float getInterpolation(float t) {
    300             t -= 1.0f;
    301             float output = t * t * t;
    302             if (mSteeper) {
    303                 output *= t * t; // Make interpolation initial slope steeper
    304             }
    305             return output + 1;
    306         }
    307     }
    308 }
    309