summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/touch
diff options
context:
space:
mode:
authorHyunyoung Song <hyunyoungs@google.com>2017-07-06 12:35:55 -0700
committerHyunyoung Song <hyunyoungs@google.com>2017-07-06 14:32:35 -0700
commitf40e94955cba0ca351f587358b9e07496d132a1b (patch)
treeada5929c818f0fb9db689af4154bd144d7c39ab6 /src/com/android/launcher3/touch
parent19b3165c890ecb897a2c3f519db78aba1b5d64df (diff)
downloadandroid_packages_apps_Trebuchet-f40e94955cba0ca351f587358b9e07496d132a1b.tar.gz
android_packages_apps_Trebuchet-f40e94955cba0ca351f587358b9e07496d132a1b.tar.bz2
android_packages_apps_Trebuchet-f40e94955cba0ca351f587358b9e07496d132a1b.zip
Add tests to SwipeDetector (formerly VerticalPullDetector).
Change-Id: I09ab4f22d7204ad806825ab0d6374c2b9616bf39
Diffstat (limited to 'src/com/android/launcher3/touch')
-rw-r--r--src/com/android/launcher3/touch/SwipeDetector.java303
1 files changed, 303 insertions, 0 deletions
diff --git a/src/com/android/launcher3/touch/SwipeDetector.java b/src/com/android/launcher3/touch/SwipeDetector.java
new file mode 100644
index 000000000..b47065490
--- /dev/null
+++ b/src/com/android/launcher3/touch/SwipeDetector.java
@@ -0,0 +1,303 @@
+package com.android.launcher3.touch;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.animation.Interpolator;
+
+/**
+ * One dimensional scroll/drag/swipe gesture detector.
+ */
+public class SwipeDetector {
+
+ private static final boolean DBG = false;
+ private static final String TAG = "SwipeDetector";
+
+ private final float mTouchSlop;
+
+ private int mScrollConditions;
+ public static final int DIRECTION_UP = 1 << 0;
+ public static final int DIRECTION_DOWN = 1 << 1;
+ public static final int DIRECTION_BOTH = DIRECTION_DOWN | DIRECTION_UP;
+
+ private static final float ANIMATION_DURATION = 1200;
+ private static final float FAST_FLING_PX_MS = 10;
+
+ /**
+ * The minimum release velocity in pixels per millisecond that triggers fling..
+ */
+ public 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 ScrollState mState = ScrollState.IDLE;
+
+ enum ScrollState {
+ IDLE,
+ DRAGGING, // onDragStart, onDrag
+ SETTLING // onDragEnd
+ }
+
+ //------------------- ScrollState transition diagram -----------------------------------
+ //
+ // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING
+ // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
+ // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
+ // SETTLING -> (View settled) -> IDLE
+
+ private void setState(ScrollState newState) {
+ if (DBG) {
+ Log.d(TAG, "setState:" + mState + "->" + newState);
+ }
+ // onDragStart and onDragEnd is reported ONLY on state transition
+ if (newState == ScrollState.DRAGGING) {
+ initializeDragging();
+ if (mState == ScrollState.IDLE) {
+ reportDragStart(false /* recatch */);
+ } else if (mState == ScrollState.SETTLING) {
+ reportDragStart(true /* recatch */);
+ }
+ }
+ if (newState == ScrollState.SETTLING) {
+ reportDragEnd();
+ }
+
+ mState = newState;
+ }
+
+ public boolean isDraggingOrSettling() {
+ return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
+ }
+
+ /**
+ * There's no touch and there's no animation.
+ */
+ public boolean isIdleState() {
+ return mState == ScrollState.IDLE;
+ }
+
+ public boolean isSettlingState() {
+ return mState == ScrollState.SETTLING;
+ }
+
+ public boolean isDraggingState() {
+ return mState == ScrollState.DRAGGING;
+ }
+
+ private float mDownX;
+ private float mDownY;
+
+ private float mLastY;
+ private long mCurrentMillis;
+
+ private float mVelocity;
+ private float mLastDisplacement;
+ private float mDisplacementY;
+ private float mDisplacementX;
+
+ private float mSubtractDisplacement;
+ private boolean mIgnoreSlopWhenSettling;
+
+ /* Client of this gesture detector can register a callback. */
+ private Listener mListener;
+
+ public void setListener(Listener l) {
+ mListener = l;
+ }
+
+ public interface Listener {
+ void onDragStart(boolean start);
+
+ boolean onDrag(float displacement, float velocity);
+
+ void onDragEnd(float velocity, boolean fling);
+ }
+
+ public SwipeDetector(Context context) {
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ }
+
+ public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
+ mScrollConditions = scrollDirectionFlags;
+ mIgnoreSlopWhenSettling = ignoreSlop;
+ }
+
+ private boolean shouldScrollStart() {
+ // reject cases where the slop condition is not met.
+ if (Math.abs(mDisplacementY) < mTouchSlop) {
+ return false;
+ }
+
+ // reject cases where the angle condition is not met.
+ float deltaY = Math.abs(mDisplacementY);
+ float deltaX = Math.max(Math.abs(mDisplacementX), 1);
+ if (deltaX > deltaY) {
+ return false;
+ }
+ // Check if the client is interested in scroll in current direction.
+ if (((mScrollConditions & DIRECTION_DOWN) > 0 && mDisplacementY > 0) ||
+ ((mScrollConditions & DIRECTION_UP) > 0 && mDisplacementY < 0)) {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownX = ev.getX();
+ mDownY = ev.getY();
+ mLastDisplacement = 0;
+ mDisplacementY = 0;
+ mVelocity = 0;
+
+ if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
+ setState(ScrollState.DRAGGING);
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ mDisplacementX = ev.getX() - mDownX;
+ mDisplacementY = ev.getY() - mDownY;
+ computeVelocity(ev);
+
+ // handle state and listener calls.
+ if (mState != ScrollState.DRAGGING && shouldScrollStart()) {
+ setState(ScrollState.DRAGGING);
+ }
+ if (mState == ScrollState.DRAGGING) {
+ reportDragging();
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ // These are synthetic events and there is no need to update internal values.
+ if (mState == ScrollState.DRAGGING) {
+ setState(ScrollState.SETTLING);
+ }
+ break;
+ default:
+ //TODO: add multi finger tracking by tracking active pointer.
+ break;
+ }
+ // Do house keeping.
+ mLastDisplacement = mDisplacementY;
+ mLastY = ev.getY();
+ return true;
+ }
+
+ public void finishedScrolling() {
+ setState(ScrollState.IDLE);
+ }
+
+ private boolean reportDragStart(boolean recatch) {
+ mListener.onDragStart(!recatch);
+ if (DBG) {
+ Log.d(TAG, "onDragStart recatch:" + recatch);
+ }
+ return true;
+ }
+
+ private void initializeDragging() {
+ if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
+ mSubtractDisplacement = 0;
+ }
+ if (mDisplacementY > 0) {
+ mSubtractDisplacement = mTouchSlop;
+ } else {
+ mSubtractDisplacement = -mTouchSlop;
+ }
+ }
+
+ private boolean reportDragging() {
+ float delta = mDisplacementY - mLastDisplacement;
+ if (delta != 0) {
+ if (DBG) {
+ Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f",
+ mDisplacementY, mVelocity));
+ }
+
+ return mListener.onDrag(mDisplacementY - mSubtractDisplacement, mVelocity);
+ }
+ return true;
+ }
+
+ private void reportDragEnd() {
+ if (DBG) {
+ Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
+ mDisplacementY, mVelocity));
+ }
+ mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS);
+
+ }
+
+ /**
+ * Computes the damped velocity using the two motion events and the previous velocity.
+ */
+ private float computeVelocity(MotionEvent to) {
+ return computeVelocity(to.getY() - mLastY, to.getEventTime());
+ }
+
+ public float computeVelocity(float delta, long currentMillis) {
+ long previousMillis = mCurrentMillis;
+ mCurrentMillis = currentMillis;
+
+ float deltaTimeMillis = mCurrentMillis - previousMillis;
+ float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0;
+ if (Math.abs(mVelocity) < 0.001f) {
+ mVelocity = velocity;
+ } else {
+ float alpha = computeDampeningFactor(deltaTimeMillis);
+ mVelocity = interpolate(mVelocity, velocity, alpha);
+ }
+ return mVelocity;
+ }
+
+ /**
+ * 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;
+ }
+
+ public long calculateDuration(float velocity, float progressNeeded) {
+ // TODO: make these values constants after tuning.
+ float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
+ float travelDistance = Math.max(0.2f, progressNeeded);
+ long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
+ if (DBG) {
+ Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
+ }
+ return duration;
+ }
+
+ public static class ScrollInterpolator implements Interpolator {
+
+ boolean mSteeper;
+
+ public void setVelocityAtZero(float velocity) {
+ mSteeper = velocity > FAST_FLING_PX_MS;
+ }
+
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ float output = t * t * t;
+ if (mSteeper) {
+ output *= t * t; // Make interpolation initial slope steeper
+ }
+ return output + 1;
+ }
+ }
+}