Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 769c795e authored by Andy Wickham's avatar Andy Wickham
Browse files

Some cleanup for SwipeDetector.

It is now organized as follows:
 - private constants
 - public constants
 - private final fields
 - private variable fields
 - constructors
 - public methods
 - private methods
 - public interface/abstract class

This is intended to be a functional no-op.

Bug: 141939911
Change-Id: Iad5a9b3b73b35641f8a4f1d52ada6adef3825c47
Tested: Built and sanity checked manually.
parent 4c420047
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.launcher3.notification;

import static com.android.launcher3.touch.SwipeDetector.HORIZONTAL;

import android.app.Notification;
import android.content.Context;
import android.graphics.Color;
@@ -33,8 +35,6 @@ import com.android.launcher3.util.Themes;

import java.util.List;

import static com.android.launcher3.touch.SwipeDetector.HORIZONTAL;

/**
 * Utility class to manage notification UI
 */
+130 −145
Original line number Diff line number Diff line
@@ -24,12 +24,11 @@ import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;

import com.android.launcher3.Utilities;
import com.android.launcher3.testing.TestProtocol;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import com.android.launcher3.Utilities;

/**
 * One dimensional scroll/drag/swipe gesture detector.
 *
@@ -41,47 +40,14 @@ public class SwipeDetector {

    private static final boolean DBG = false;
    private static final String TAG = "SwipeDetector";
    private static final float ANIMATION_DURATION = 1200;
    /** The minimum release velocity in pixels per millisecond that triggers fling.*/
    private static final float RELEASE_VELOCITY_PX_MS = 1.0f;

    private int mScrollConditions;
    public static final int DIRECTION_POSITIVE = 1 << 0;
    public static final int DIRECTION_NEGATIVE = 1 << 1;
    public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;

    private static final float ANIMATION_DURATION = 1200;

    protected int mActivePointerId = INVALID_POINTER_ID;

    /**
     * The minimum release velocity in pixels per millisecond that triggers fling..
     */
    public static final float RELEASE_VELOCITY_PX_MS = 1.0f;

    /* Scroll state, this is set to true during dragging and animation. */
    private ScrollState mState = ScrollState.IDLE;

    enum ScrollState {
        IDLE,
        DRAGGING,      // onDragStart, onDrag
        SETTLING       // onDragEnd
    }

    public static abstract class Direction {

        abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint,
                boolean isRtl);

        /**
         * Distance in pixels a touch can wander before we think the user is scrolling.
         */
        abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);

        abstract float getVelocity(VelocityTracker tracker, boolean isRtl);

        abstract boolean isPositive(float displacement);

        abstract boolean isNegative(float displacement);
    }

    public static final Direction VERTICAL = new Direction() {

        @Override
@@ -150,35 +116,54 @@ public class SwipeDetector {
        }
    };

    //------------------- 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 final PointF mDownPos = new PointF();
    private final PointF mLastPos = new PointF();
    private final Direction mDir;
    private final boolean mIsRtl;
    private final float mTouchSlop;
    private final float mMaxVelocity;
    /* Client of this gesture detector can register a callback. */
    private final Listener mListener;

    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 */);
            }
    private int mActivePointerId = INVALID_POINTER_ID;
    private VelocityTracker mVelocityTracker;
    private float mLastDisplacement;
    private float mDisplacement;
    private float mSubtractDisplacement;
    private boolean mIgnoreSlopWhenSettling;
    private int mScrollDirections;
    private ScrollState mState = ScrollState.IDLE;

    private enum ScrollState {
        IDLE,
        DRAGGING,      // onDragStart, onDrag
        SETTLING       // onDragEnd
    }
        if (newState == ScrollState.SETTLING) {
            reportDragEnd();

    public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
        this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
    }

        mState = newState;
    @VisibleForTesting
    protected SwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
            @NonNull Direction dir, boolean isRtl) {
        mListener = l;
        mDir = dir;
        mIsRtl = isRtl;
        mTouchSlop = config.getScaledTouchSlop();
        mMaxVelocity = config.getScaledMaximumFlingVelocity();
    }

    public boolean isDraggingOrSettling() {
        return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
    public static 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 int getDownX() {
@@ -203,73 +188,31 @@ public class SwipeDetector {
        return mState == ScrollState.DRAGGING;
    }

    private final PointF mDownPos = new PointF();
    private final PointF mLastPos = new PointF();
    private final Direction mDir;
    private final boolean mIsRtl;

    private final float mTouchSlop;
    private final float mMaxVelocity;

    /* Client of this gesture detector can register a callback. */
    private final Listener mListener;

    private VelocityTracker mVelocityTracker;

    private float mLastDisplacement;
    private float mDisplacement;

    private float mSubtractDisplacement;
    private boolean mIgnoreSlopWhenSettling;

    public interface Listener {
        void onDragStart(boolean start);

        boolean onDrag(float displacement);

        default boolean onDrag(float displacement, MotionEvent event) {
            return onDrag(displacement);
        }

        void onDragEnd(float velocity, boolean fling);
    }

    public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
        this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
    }

    @VisibleForTesting
    protected SwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
            @NonNull Direction dir, boolean isRtl) {
        mListener = l;
        mDir = dir;
        mIsRtl = isRtl;
        mTouchSlop = config.getScaledTouchSlop();
        mMaxVelocity = config.getScaledMaximumFlingVelocity();
    public boolean isDraggingOrSettling() {
        return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
    }

    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
        mScrollConditions = scrollDirectionFlags;
        mScrollDirections = scrollDirectionFlags;
        mIgnoreSlopWhenSettling = ignoreSlop;
    }

    public int getScrollDirections() {
        return mScrollConditions;
        return mScrollDirections;
    }

    private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
        // reject cases where the angle or slop condition is not met.
        if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
                > Math.abs(mDisplacement)) {
            return false;
    public void finishedScrolling() {
        setState(ScrollState.IDLE);
    }

        // Check if the client is interested in scroll in current direction.
        if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(mDisplacement)) ||
                ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDir.isPositive(mDisplacement))) {
            return true;
        }
        return false;
    /**
     * Returns if the start drag was towards the positive direction or negative.
     *
     * @see #setDetectableScrollConditions(int, boolean)
     * @see #DIRECTION_BOTH
     */
    public boolean wasInitialTouchPositive() {
        return mDir.isPositive(mSubtractDisplacement);
    }

    public boolean onTouchEvent(MotionEvent ev) {
@@ -338,16 +281,50 @@ public class SwipeDetector {
        return true;
    }

    public void finishedScrolling() {
        setState(ScrollState.IDLE);
    //------------------- 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;
    }

    private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
        // reject cases where the angle or slop condition is not met.
        if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
                > Math.abs(mDisplacement)) {
            return false;
        }

        // Check if the client is interested in scroll in current direction.
        return ((mScrollDirections & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(mDisplacement))
                || ((mScrollDirections & DIRECTION_POSITIVE) > 0 && mDir.isPositive(mDisplacement));
    }

    private boolean reportDragStart(boolean recatch) {
    private void reportDragStart(boolean recatch) {
        mListener.onDragStart(!recatch);
        if (DBG) {
            Log.d(TAG, "onDragStart recatch:" + recatch);
        }
        return true;
    }

    private void initializeDragging() {
@@ -361,26 +338,15 @@ public class SwipeDetector {
        }
    }

    /**
     * Returns if the start drag was towards the positive direction or negative.
     *
     * @see #setDetectableScrollConditions(int, boolean)
     * @see #DIRECTION_BOTH
     */
    public boolean wasInitialTouchPositive() {
        return mDir.isPositive(mSubtractDisplacement);
    }

    private boolean reportDragging(MotionEvent event) {
    private void reportDragging(MotionEvent event) {
        if (mDisplacement != mLastDisplacement) {
            if (DBG) {
                Log.d(TAG, String.format("onDrag disp=%.1f", mDisplacement));
            }

            mLastDisplacement = mDisplacement;
            return mListener.onDrag(mDisplacement - mSubtractDisplacement, event);
            mListener.onDrag(mDisplacement - mSubtractDisplacement, event);
        }
        return true;
    }

    private void reportDragEnd() {
@@ -394,14 +360,33 @@ public class SwipeDetector {
        mListener.onDragEnd(velocity, Math.abs(velocity) > RELEASE_VELOCITY_PX_MS);
    }

    public static 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));
    /** Listener to receive updates on the swipe. */
    public interface Listener {
        void onDragStart(boolean start);

        boolean onDrag(float displacement);

        default boolean onDrag(float displacement, MotionEvent event) {
            return onDrag(displacement);
        }
        return duration;

        void onDragEnd(float velocity, boolean fling);
    }

    public abstract static class Direction {

        abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint,
                boolean isRtl);

        /**
         * Distance in pixels a touch can wander before we think the user is scrolling.
         */
        abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);

        abstract float getVelocity(VelocityTracker tracker, boolean isRtl);

        abstract boolean isPositive(float displacement);

        abstract boolean isNegative(float displacement);
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -25,6 +25,10 @@ import static org.mockito.Mockito.verify;
import android.util.Log;
import android.view.ViewConfiguration;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.launcher3.testcomponent.TouchEventGenerator;

import org.junit.Before;
@@ -33,10 +37,6 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class SwipeDetectorTest {