| package com.android.launcher3.allapps; |
| |
| import android.content.Context; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.ViewConfiguration; |
| |
| /** |
| * One dimensional scroll gesture detector for all apps container pull up interaction. |
| */ |
| public class VerticalPullDetector { |
| |
| private static final String TAG = "ScrollGesture"; |
| private static final boolean DBG = false; |
| |
| private float mTouchSlop; |
| private boolean mScrollDown; // if false, only scroll up will be reported. |
| private boolean mDisallowRecatchFromTop; |
| private boolean mDisallowRecatchFromBottom; |
| |
| /** |
| * The minimum release velocity in pixels per millisecond that triggers fling.. |
| */ |
| private static final float RELEASE_VELOCITY_PX_MS = 1.0f; |
| |
| /** |
| * The time constant used to calculate dampening in the low-pass filter of scroll velocity. |
| * Cutoff frequency is set at 10 Hz. |
| */ |
| public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10); |
| |
| /* Scroll state, this is set to true during dragging and animation. */ |
| private State mState = State.NONE; |
| enum State {NONE, DRAG, SCROLLING}; |
| |
| private void setState(State newState) { |
| if (DBG) { |
| Log.d(TAG, mState + "->" + newState); |
| } |
| mState = newState; |
| } |
| |
| public boolean shouldIntercept() { |
| return mState == State.DRAG; |
| } |
| |
| public boolean isRestingState() { |
| return mState == State.NONE; |
| } |
| |
| private float mDownX; |
| private float mDownY; |
| private float mDownMillis; |
| |
| private float mLastY; |
| private float mLastMillis; |
| |
| private float mVelocity; |
| private float mLastDisplacement; |
| private float mDisplacementY; |
| private float mDisplacementX; |
| |
| /* scroll started during previous animation */ |
| private boolean mSubtractSlop = true; |
| |
| /* Client of this gesture detector can register a callback. */ |
| Listener mListener; |
| |
| public void setListener(Listener l) { |
| mListener = l; |
| } |
| |
| interface Listener{ |
| void onScrollStart(boolean start); |
| boolean onScroll(float displacement, float velocity); |
| void onScrollEnd(float velocity, boolean fling); |
| } |
| |
| public VerticalPullDetector(Context context) { |
| mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); |
| } |
| |
| public void setDetectableScrollConditions(boolean scrollDown, boolean disallowRecatchFromTop, |
| boolean disallowRecatchFromBottom) { |
| mScrollDown = scrollDown; |
| mDisallowRecatchFromTop = disallowRecatchFromTop; |
| mDisallowRecatchFromBottom = disallowRecatchFromBottom; |
| } |
| |
| private boolean shouldScrollStart() { |
| float deltaY = Math.abs(mDisplacementY); |
| float deltaX = Math.max(Math.abs(mDisplacementX), 1); |
| if (mScrollDown && mDisplacementY > mTouchSlop) { |
| if (deltaY > deltaX) { |
| return true; |
| } |
| } |
| if (!mScrollDown && mDisplacementY < -mTouchSlop) { |
| if (deltaY > deltaX) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean shouldRecatchScrollStart() { |
| if (!mDisallowRecatchFromBottom && !mDisallowRecatchFromTop) { |
| return true; |
| } |
| if (mDisallowRecatchFromTop && mDisplacementY > mTouchSlop) { |
| mDisallowRecatchFromTop = false; |
| return true; |
| } |
| if (mDisallowRecatchFromBottom && mDisplacementY < -mTouchSlop) { |
| mDisallowRecatchFromBottom = false; |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean onTouchEvent(MotionEvent ev) { |
| switch (ev.getAction()) { |
| case MotionEvent.ACTION_DOWN: |
| mDownMillis = ev.getDownTime(); |
| mDownX = ev.getX(); |
| mDownY = ev.getY(); |
| mLastDisplacement = 0; |
| mVelocity = 0; |
| if (mState == State.SCROLLING && shouldRecatchScrollStart()){ |
| reportScrollStart(true /* recatch */); |
| } |
| break; |
| case MotionEvent.ACTION_MOVE: |
| mDisplacementX = ev.getX() - mDownX; |
| mDisplacementY = ev.getY() - mDownY; |
| mVelocity = computeVelocity(ev, mVelocity); |
| |
| if (mState == State.SCROLLING && Math.abs(mDisplacementY) > mTouchSlop ){ |
| setState(State.DRAG); |
| reportScrollStart(true /* recatch */); |
| } |
| if (mState == State.NONE && shouldScrollStart()) { |
| setState(State.DRAG); |
| reportScrollStart(false /* recatch */); |
| } |
| if (mState == State.DRAG && mListener != null) { |
| reportScroll(); |
| } |
| break; |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: |
| // These are synthetic events and there is no need to update internal values. |
| if (mState == State.DRAG && mListener != null) { |
| reportScrollEnd(); |
| } |
| break; |
| default: |
| //TODO: add multi finger tracking by tracking active pointer. |
| break; |
| } |
| // Do house keeping. |
| mLastDisplacement = mDisplacementY; |
| |
| mLastY = ev.getY(); |
| mLastMillis = ev.getEventTime(); |
| |
| return true; |
| } |
| |
| public void finishedScrolling() { |
| setState(State.NONE); |
| } |
| |
| private boolean reportScrollStart(boolean recatch) { |
| mListener.onScrollStart(!recatch); |
| mSubtractSlop = !recatch; |
| if (DBG) { |
| Log.d(TAG, "onScrollStart recatch:" + recatch); |
| } |
| return true; |
| } |
| |
| private boolean reportScroll() { |
| float delta = mDisplacementY - mLastDisplacement; |
| if (delta != 0) { |
| if (DBG) { |
| Log.d(TAG, String.format("onScroll disp=%.1f, velocity=%.1f", |
| mDisplacementY, mVelocity)); |
| } |
| float subtractDisplacement = 0f; |
| if (mSubtractSlop) { |
| if (mDisplacementY > 0) { |
| subtractDisplacement = mTouchSlop; |
| } else { |
| subtractDisplacement = -mTouchSlop; |
| } |
| } |
| return mListener.onScroll(mDisplacementY - subtractDisplacement, mVelocity); |
| } |
| return true; |
| } |
| |
| private void reportScrollEnd() { |
| if (DBG) { |
| Log.d(TAG, String.format("onScrolEnd disp=%.1f, velocity=%.1f", |
| mDisplacementY, mVelocity)); |
| } |
| mListener.onScrollEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS); |
| setState(State.SCROLLING); |
| } |
| /** |
| * Computes the damped velocity using the two motion events and the previous velocity. |
| */ |
| private float computeVelocity(MotionEvent to, float previousVelocity) { |
| float delta = computeDelta(to); |
| |
| float deltaTimeMillis = to.getEventTime() - mLastMillis; |
| float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0; |
| if (Math.abs(previousVelocity) < 0.001f) { |
| return velocity; |
| } |
| |
| float alpha = computeDampeningFactor(deltaTimeMillis); |
| return interpolate(previousVelocity, velocity, alpha); |
| } |
| |
| private float computeDelta(MotionEvent to) { |
| return to.getY() - mLastY; |
| } |
| |
| /** |
| * Returns a time-dependent dampening factor using delta time. |
| */ |
| private static float computeDampeningFactor(float deltaTime) { |
| return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime); |
| } |
| |
| /** |
| * Returns the linear interpolation between two values |
| */ |
| private static float interpolate(float from, float to, float alpha) { |
| return (1.0f - alpha) * from + alpha * to; |
| } |
| } |