From eed585b0c3ac027719121ec064d22026f3930691 Mon Sep 17 00:00:00 2001 From: Tony Wickham Date: Thu, 15 Aug 2019 17:24:07 -0700 Subject: Refactor SwipeDetector to track both axes Existing clients now use the SingleAxisSwipeDetector subclass. A followup CL will add BothAxesSwipeDetector, whose first client will be the quick switch from home controller. Bug: 126596417 Change-Id: I54c71088cfe99ff28cdc719a1eb7a7d06ac95d2d Merged-In: I54c71088cfe99ff28cdc719a1eb7a7d06ac95d2d --- .../notification/NotificationItemView.java | 10 +- .../notification/NotificationMainView.java | 17 +- .../touch/AbstractStateChangeTouchController.java | 23 +- .../android/launcher3/touch/BaseSwipeDetector.java | 267 ++++++++++++++ .../launcher3/touch/SingleAxisSwipeDetector.java | 189 ++++++++++ src/com/android/launcher3/touch/SwipeDetector.java | 391 --------------------- .../launcher3/views/AbstractSlideInView.java | 22 +- 7 files changed, 494 insertions(+), 425 deletions(-) create mode 100644 src/com/android/launcher3/touch/BaseSwipeDetector.java create mode 100644 src/com/android/launcher3/touch/SingleAxisSwipeDetector.java delete mode 100644 src/com/android/launcher3/touch/SwipeDetector.java (limited to 'src') diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java index 717a7e93d..021fb30c5 100644 --- a/src/com/android/launcher3/notification/NotificationItemView.java +++ b/src/com/android/launcher3/notification/NotificationItemView.java @@ -16,7 +16,7 @@ package com.android.launcher3.notification; -import static com.android.launcher3.touch.SwipeDetector.HORIZONTAL; +import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL; import android.app.Notification; import android.content.Context; @@ -30,7 +30,7 @@ import android.widget.TextView; import com.android.launcher3.R; import com.android.launcher3.graphics.IconPalette; import com.android.launcher3.popup.PopupContainerWithArrow; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.util.Themes; import java.util.List; @@ -49,7 +49,7 @@ public class NotificationItemView { private final TextView mHeaderCount; private final NotificationMainView mMainView; private final NotificationFooterLayout mFooter; - private final SwipeDetector mSwipeDetector; + private final SingleAxisSwipeDetector mSwipeDetector; private final View mIconView; private final View mHeader; @@ -74,8 +74,8 @@ public class NotificationItemView { mHeader = container.findViewById(R.id.header); mDivider = container.findViewById(R.id.divider); - mSwipeDetector = new SwipeDetector(mContext, mMainView, HORIZONTAL); - mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false); + mSwipeDetector = new SingleAxisSwipeDetector(mContext, mMainView, HORIZONTAL); + mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false); mMainView.setSwipeDetector(mSwipeDetector); mFooter.setContainer(this); } diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java index 78627ecc1..b67adbb2c 100644 --- a/src/com/android/launcher3/notification/NotificationMainView.java +++ b/src/com/android/launcher3/notification/NotificationMainView.java @@ -38,8 +38,9 @@ import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.OverScroll; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.util.Themes; @@ -48,7 +49,7 @@ import com.android.launcher3.util.Themes; * e.g. icon + title + text. */ @TargetApi(Build.VERSION_CODES.N) -public class NotificationMainView extends FrameLayout implements SwipeDetector.Listener { +public class NotificationMainView extends FrameLayout implements SingleAxisSwipeDetector.Listener { private static FloatProperty CONTENT_TRANSLATION = new FloatProperty("contentTranslation") { @@ -75,7 +76,7 @@ public class NotificationMainView extends FrameLayout implements SwipeDetector.L private TextView mTextView; private View mIconView; - private SwipeDetector mSwipeDetector; + private SingleAxisSwipeDetector mSwipeDetector; public NotificationMainView(Context context) { this(context, null, 0); @@ -107,7 +108,7 @@ public class NotificationMainView extends FrameLayout implements SwipeDetector.L mIconView = findViewById(R.id.popup_item_icon); } - public void setSwipeDetector(SwipeDetector swipeDetector) { + public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) { mSwipeDetector = swipeDetector; } @@ -173,7 +174,7 @@ public class NotificationMainView extends FrameLayout implements SwipeDetector.L LauncherLogProto.ItemType.NOTIFICATION); } - // SwipeDetector.Listener's + // SingleAxisSwipeDetector.Listener's @Override public void onDragStart(boolean start) { } @@ -187,7 +188,7 @@ public class NotificationMainView extends FrameLayout implements SwipeDetector.L } @Override - public void onDragEnd(float velocity, boolean fling) { + public void onDragEnd(float velocity) { final boolean willExit; final float endTranslation; final float startTranslation = mTextAndBackground.getTranslationX(); @@ -195,7 +196,7 @@ public class NotificationMainView extends FrameLayout implements SwipeDetector.L if (!canChildBeDismissed()) { willExit = false; endTranslation = 0; - } else if (fling) { + } else if (mSwipeDetector.isFling(velocity)) { willExit = true; endTranslation = velocity < 0 ? - getWidth() : getWidth(); } else if (Math.abs(startTranslation) > getWidth() / 2) { @@ -206,7 +207,7 @@ public class NotificationMainView extends FrameLayout implements SwipeDetector.L endTranslation = 0; } - long duration = SwipeDetector.calculateDuration(velocity, + long duration = BaseSwipeDetector.calculateDuration(velocity, (endTranslation - startTranslation) / getWidth()); mContentTranslateAnimator.removeAllListeners(); diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java index c5ba5bab6..60f6ee9c5 100644 --- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java +++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java @@ -53,7 +53,7 @@ import com.android.launcher3.util.TouchController; * TouchController for handling state changes */ public abstract class AbstractStateChangeTouchController - implements TouchController, SwipeDetector.Listener { + implements TouchController, SingleAxisSwipeDetector.Listener { // Progress after which the transition is assumed to be a success in case user does not fling public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; @@ -65,8 +65,8 @@ public abstract class AbstractStateChangeTouchController protected final long ATOMIC_DURATION = getAtomicDuration(); protected final Launcher mLauncher; - protected final SwipeDetector mDetector; - protected final SwipeDetector.Direction mSwipeDirection; + protected final SingleAxisSwipeDetector mDetector; + protected final SingleAxisSwipeDetector.Direction mSwipeDirection; private boolean mNoIntercept; private boolean mIsLogContainerSet; @@ -101,9 +101,9 @@ public abstract class AbstractStateChangeTouchController private float mAtomicComponentsStartProgress; - public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) { + public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) { mLauncher = l; - mDetector = new SwipeDetector(l, this, dir); + mDetector = new SingleAxisSwipeDetector(l, this, dir); mSwipeDirection = dir; } @@ -127,7 +127,7 @@ public abstract class AbstractStateChangeTouchController boolean ignoreSlopWhenSettling = false; if (mCurrentAnimation != null) { - directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; + directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH; ignoreSlopWhenSettling = true; } else { directionsToDetectScroll = getSwipeDirection(); @@ -152,10 +152,10 @@ public abstract class AbstractStateChangeTouchController LauncherState fromState = mLauncher.getStateManager().getState(); int swipeDirection = 0; if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) { - swipeDirection |= SwipeDetector.DIRECTION_POSITIVE; + swipeDirection |= SingleAxisSwipeDetector.DIRECTION_POSITIVE; } if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) { - swipeDirection |= SwipeDetector.DIRECTION_NEGATIVE; + swipeDirection |= SingleAxisSwipeDetector.DIRECTION_NEGATIVE; } return swipeDirection; } @@ -369,7 +369,8 @@ public abstract class AbstractStateChangeTouchController } @Override - public void onDragEnd(float velocity, boolean fling) { + public void onDragEnd(float velocity) { + boolean fling = mDetector.isFling(velocity); final int logAction = fling ? Touch.FLING : Touch.SWIPE; boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); @@ -406,7 +407,7 @@ public abstract class AbstractStateChangeTouchController } else { startProgress = Utilities.boundToRange(progress + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f); - duration = SwipeDetector.calculateDuration(velocity, + duration = BaseSwipeDetector.calculateDuration(velocity, endProgress - Math.max(progress, 0)) * durationMultiplier; } } else { @@ -424,7 +425,7 @@ public abstract class AbstractStateChangeTouchController } else { startProgress = Utilities.boundToRange(progress + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f); - duration = SwipeDetector.calculateDuration(velocity, + duration = BaseSwipeDetector.calculateDuration(velocity, Math.min(progress, 1) - endProgress) * durationMultiplier; } } diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java new file mode 100644 index 000000000..08d73d073 --- /dev/null +++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.touch; + +import static android.view.MotionEvent.INVALID_POINTER_ID; + +import android.graphics.PointF; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import androidx.annotation.NonNull; + +/** + * Scroll/drag/swipe gesture detector. + * + * Definition of swipe is different from android system in that this detector handles + * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before + * swipe action happens. + * + * @see SingleAxisSwipeDetector + */ +public abstract class BaseSwipeDetector { + + private static final boolean DBG = false; + private static final String TAG = "BaseSwipeDetector"; + 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 static final PointF sTempPoint = new PointF(); + + private final PointF mDownPos = new PointF(); + private final PointF mLastPos = new PointF(); + protected final boolean mIsRtl; + protected final float mTouchSlop; + protected final float mMaxVelocity; + + private int mActivePointerId = INVALID_POINTER_ID; + private VelocityTracker mVelocityTracker; + private PointF mLastDisplacement = new PointF(); + private PointF mDisplacement = new PointF(); + protected PointF mSubtractDisplacement = new PointF(); + private ScrollState mState = ScrollState.IDLE; + + protected boolean mIgnoreSlopWhenSettling; + + private enum ScrollState { + IDLE, + DRAGGING, // onDragStart, onDrag + SETTLING // onDragEnd + } + + protected BaseSwipeDetector(@NonNull ViewConfiguration config, boolean isRtl) { + mTouchSlop = config.getScaledTouchSlop(); + mMaxVelocity = config.getScaledMaximumFlingVelocity(); + mIsRtl = isRtl; + } + + 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() { + return (int) mDownPos.x; + } + + public int getDownY() { + return (int) mDownPos.y; + } + /** + * 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; + } + + public boolean isDraggingOrSettling() { + return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING; + } + + public void finishedScrolling() { + setState(ScrollState.IDLE); + } + + public boolean isFling(float velocity) { + return Math.abs(velocity) > RELEASE_VELOCITY_PX_MS; + } + + public boolean onTouchEvent(MotionEvent ev) { + int actionMasked = ev.getActionMasked(); + if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) { + mVelocityTracker.clear(); + } + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = ev.getPointerId(0); + mDownPos.set(ev.getX(), ev.getY()); + mLastPos.set(mDownPos); + mLastDisplacement.set(0, 0); + mDisplacement.set(0, 0); + + if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { + setState(ScrollState.DRAGGING); + } + break; + //case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_POINTER_UP: + int ptrIdx = ev.getActionIndex(); + int ptrId = ev.getPointerId(ptrIdx); + if (ptrId == mActivePointerId) { + final int newPointerIdx = ptrIdx == 0 ? 1 : 0; + mDownPos.set( + ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), + ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); + mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); + mActivePointerId = ev.getPointerId(newPointerIdx); + } + break; + case MotionEvent.ACTION_MOVE: + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == INVALID_POINTER_ID) { + break; + } + mDisplacement.set(ev.getX(pointerIndex) - mDownPos.x, + ev.getY(pointerIndex) - mDownPos.y); + if (mIsRtl) { + mDisplacement.x = -mDisplacement.x; + } + + // handle state and listener calls. + if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) { + setState(ScrollState.DRAGGING); + } + if (mState == ScrollState.DRAGGING) { + reportDragging(ev); + } + mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); + 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); + } + mVelocityTracker.recycle(); + mVelocityTracker = null; + break; + default: + break; + } + return true; + } + + //------------------- 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 void initializeDragging() { + if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { + mSubtractDisplacement.set(0, 0); + } else { + mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop; + mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop; + } + } + + protected abstract boolean shouldScrollStart(PointF displacement); + + private void reportDragStart(boolean recatch) { + reportDragStartInternal(recatch); + if (DBG) { + Log.d(TAG, "onDragStart recatch:" + recatch); + } + } + + protected abstract void reportDragStartInternal(boolean recatch); + + private void reportDragging(MotionEvent event) { + if (mDisplacement != mLastDisplacement) { + if (DBG) { + Log.d(TAG, String.format("onDrag disp=%s", mDisplacement)); + } + + mLastDisplacement.set(mDisplacement); + sTempPoint.set(mDisplacement.x - mSubtractDisplacement.x, + mDisplacement.y - mSubtractDisplacement.y); + reportDraggingInternal(sTempPoint, event); + } + } + + protected abstract void reportDraggingInternal(PointF displacement, MotionEvent event); + + private void reportDragEnd() { + mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); + PointF velocity = new PointF(mVelocityTracker.getXVelocity() / 1000, + mVelocityTracker.getYVelocity() / 1000); + if (mIsRtl) { + velocity.x = -velocity.x; + } + if (DBG) { + Log.d(TAG, String.format("onScrollEnd disp=%.1s, velocity=%.1s", + mDisplacement, velocity)); + } + + reportDragEndInternal(velocity); + } + + protected abstract void reportDragEndInternal(PointF velocity); +} diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java new file mode 100644 index 000000000..0bf2ff654 --- /dev/null +++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.touch; + +import android.content.Context; +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.launcher3.Utilities; + +/** + * One dimensional scroll/drag/swipe gesture detector (either HORIZONTAL or VERTICAL). + */ +public class SingleAxisSwipeDetector extends BaseSwipeDetector { + + 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; + + public static final Direction VERTICAL = new Direction() { + + @Override + boolean isPositive(float displacement) { + // Up + return displacement < 0; + } + + @Override + boolean isNegative(float displacement) { + // Down + return displacement > 0; + } + + @Override + float extractDirection(PointF direction) { + return direction.y; + } + + @Override + boolean canScrollStart(PointF displacement, float touchSlop) { + return Math.abs(displacement.y) >= Math.max(Math.abs(displacement.x), touchSlop); + } + + }; + + public static final Direction HORIZONTAL = new Direction() { + + @Override + boolean isPositive(float displacement) { + // Right + return displacement > 0; + } + + @Override + boolean isNegative(float displacement) { + // Left + return displacement < 0; + } + + @Override + float extractDirection(PointF direction) { + return direction.x; + } + + @Override + boolean canScrollStart(PointF displacement, float touchSlop) { + return Math.abs(displacement.x) >= Math.max(Math.abs(displacement.y), touchSlop); + } + }; + + private final Direction mDir; + /* Client of this gesture detector can register a callback. */ + private final Listener mListener; + + private int mScrollDirections; + + public SingleAxisSwipeDetector(@NonNull Context context, @NonNull Listener l, + @NonNull Direction dir) { + this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources())); + } + + @VisibleForTesting + protected SingleAxisSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l, + @NonNull Direction dir, boolean isRtl) { + super(config, isRtl); + mListener = l; + mDir = dir; + } + + public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) { + mScrollDirections = scrollDirectionFlags; + mIgnoreSlopWhenSettling = ignoreSlop; + } + + public int getScrollDirections() { + return mScrollDirections; + } + + /** + * 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(mDir.extractDirection(mSubtractDisplacement)); + } + + @Override + protected boolean shouldScrollStart(PointF displacement) { + // Reject cases where the angle or slop condition is not met. + if (!mDir.canScrollStart(displacement, mTouchSlop)) { + return false; + } + + // Check if the client is interested in scroll in current direction. + float displacementComponent = mDir.extractDirection(displacement); + return canScrollNegative(displacementComponent) || canScrollPositive(displacementComponent); + } + + private boolean canScrollNegative(float displacement) { + return (mScrollDirections & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(displacement); + } + + private boolean canScrollPositive(float displacement) { + return (mScrollDirections & DIRECTION_POSITIVE) > 0 && mDir.isPositive(displacement); + } + + @Override + protected void reportDragStartInternal(boolean recatch) { + mListener.onDragStart(!recatch); + } + + @Override + protected void reportDraggingInternal(PointF displacement, MotionEvent event) { + mListener.onDrag(mDir.extractDirection(displacement), event); + } + + @Override + protected void reportDragEndInternal(PointF velocity) { + float velocityComponent = mDir.extractDirection(velocity); + mListener.onDragEnd(velocityComponent); + } + + /** Listener to receive updates on the swipe. */ + public interface Listener { + void onDragStart(boolean start); + + // TODO remove + boolean onDrag(float displacement); + + default boolean onDrag(float displacement, MotionEvent event) { + return onDrag(displacement); + } + + void onDragEnd(float velocity); + } + + public abstract static class Direction { + + abstract boolean isPositive(float displacement); + + abstract boolean isNegative(float displacement); + + /** Returns the part of the given {@link PointF} that is relevant to this direction. */ + abstract float extractDirection(PointF point); + + /** Reject cases where the angle or slop condition is not met. */ + abstract boolean canScrollStart(PointF displacement, float touchSlop); + + } +} diff --git a/src/com/android/launcher3/touch/SwipeDetector.java b/src/com/android/launcher3/touch/SwipeDetector.java deleted file mode 100644 index c38ca24d9..000000000 --- a/src/com/android/launcher3/touch/SwipeDetector.java +++ /dev/null @@ -1,391 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.touch; - -import static android.view.MotionEvent.INVALID_POINTER_ID; - -import android.content.Context; -import android.graphics.PointF; -import android.util.Log; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.ViewConfiguration; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import com.android.launcher3.Utilities; - -/** - * One dimensional scroll/drag/swipe gesture detector. - * - * Definition of swipe is different from android system in that this detector handles - * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before - * swipe action happens - */ -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; - - 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; - - public static final Direction VERTICAL = new Direction() { - - @Override - float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) { - return ev.getY(pointerIndex) - refPoint.y; - } - - @Override - float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) { - return Math.abs(ev.getX(pointerIndex) - downPos.x); - } - - @Override - float getVelocity(VelocityTracker tracker, boolean isRtl) { - return tracker.getYVelocity(); - } - - @Override - boolean isPositive(float displacement) { - // Up - return displacement < 0; - } - - @Override - boolean isNegative(float displacement) { - // Down - return displacement > 0; - } - }; - - public static final Direction HORIZONTAL = new Direction() { - - @Override - float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) { - float displacement = ev.getX(pointerIndex) - refPoint.x; - if (isRtl) { - displacement = -displacement; - } - return displacement; - } - - @Override - float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) { - return Math.abs(ev.getY(pointerIndex) - downPos.y); - } - - @Override - float getVelocity(VelocityTracker tracker, boolean isRtl) { - float velocity = tracker.getXVelocity(); - if (isRtl) { - velocity = -velocity; - } - return velocity; - } - - @Override - boolean isPositive(float displacement) { - // Right - return displacement > 0; - } - - @Override - boolean isNegative(float displacement) { - // Left - return displacement < 0; - } - }; - - 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 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 - } - - 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 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() { - return (int) mDownPos.x; - } - - public int getDownY() { - return (int) mDownPos.y; - } - /** - * 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; - } - - public boolean isDraggingOrSettling() { - return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING; - } - - public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) { - mScrollDirections = scrollDirectionFlags; - mIgnoreSlopWhenSettling = ignoreSlop; - } - - public int getScrollDirections() { - return mScrollDirections; - } - - public void finishedScrolling() { - setState(ScrollState.IDLE); - } - - /** - * 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) { - int actionMasked = ev.getActionMasked(); - if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) { - mVelocityTracker.clear(); - } - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(ev); - - switch (actionMasked) { - case MotionEvent.ACTION_DOWN: - mActivePointerId = ev.getPointerId(0); - mDownPos.set(ev.getX(), ev.getY()); - mLastPos.set(mDownPos); - mLastDisplacement = 0; - mDisplacement = 0; - - if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { - setState(ScrollState.DRAGGING); - } - break; - //case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_POINTER_UP: - int ptrIdx = ev.getActionIndex(); - int ptrId = ev.getPointerId(ptrIdx); - if (ptrId == mActivePointerId) { - final int newPointerIdx = ptrIdx == 0 ? 1 : 0; - mDownPos.set( - ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), - ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); - mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); - mActivePointerId = ev.getPointerId(newPointerIdx); - } - break; - case MotionEvent.ACTION_MOVE: - int pointerIndex = ev.findPointerIndex(mActivePointerId); - if (pointerIndex == INVALID_POINTER_ID) { - break; - } - mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos, mIsRtl); - - // handle state and listener calls. - if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) { - setState(ScrollState.DRAGGING); - } - if (mState == ScrollState.DRAGGING) { - reportDragging(ev); - } - mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); - 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); - } - mVelocityTracker.recycle(); - mVelocityTracker = null; - break; - default: - break; - } - return true; - } - - //------------------- 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 void reportDragStart(boolean recatch) { - mListener.onDragStart(!recatch); - if (DBG) { - Log.d(TAG, "onDragStart recatch:" + recatch); - } - } - - private void initializeDragging() { - if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { - mSubtractDisplacement = 0; - } else if (mDisplacement > 0) { - mSubtractDisplacement = mTouchSlop; - } else { - mSubtractDisplacement = -mTouchSlop; - } - } - - private void reportDragging(MotionEvent event) { - if (mDisplacement != mLastDisplacement) { - if (DBG) { - Log.d(TAG, String.format("onDrag disp=%.1f", mDisplacement)); - } - - mLastDisplacement = mDisplacement; - mListener.onDrag(mDisplacement - mSubtractDisplacement, event); - } - } - - private void reportDragEnd() { - mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); - float velocity = mDir.getVelocity(mVelocityTracker, mIsRtl) / 1000; - if (DBG) { - Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f", - mDisplacement, velocity)); - } - - mListener.onDragEnd(velocity, Math.abs(velocity) > RELEASE_VELOCITY_PX_MS); - } - - /** 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); - } - - 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); - } -} diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java index a4518bae3..195a77ad5 100644 --- a/src/com/android/launcher3/views/AbstractSlideInView.java +++ b/src/com/android/launcher3/views/AbstractSlideInView.java @@ -32,13 +32,14 @@ import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.Utilities; import com.android.launcher3.anim.Interpolators; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.BaseSwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; /** * Extension of AbstractFloatingView with common methods for sliding in from bottom */ public abstract class AbstractSlideInView extends AbstractFloatingView - implements SwipeDetector.Listener { + implements SingleAxisSwipeDetector.Listener { protected static Property TRANSLATION_SHIFT = new Property(Float.class, "translationShift") { @@ -57,7 +58,7 @@ public abstract class AbstractSlideInView extends AbstractFloatingView protected static final float TRANSLATION_SHIFT_OPENED = 0f; protected final Launcher mLauncher; - protected final SwipeDetector mSwipeDetector; + protected final SingleAxisSwipeDetector mSwipeDetector; protected final ObjectAnimator mOpenCloseAnimator; protected View mContent; @@ -73,7 +74,8 @@ public abstract class AbstractSlideInView extends AbstractFloatingView mLauncher = Launcher.getLauncher(context); mScrollInterpolator = Interpolators.SCROLL_CUBIC; - mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL); + mSwipeDetector = new SingleAxisSwipeDetector(context, this, + SingleAxisSwipeDetector.VERTICAL); mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this); mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { @@ -97,7 +99,7 @@ public abstract class AbstractSlideInView extends AbstractFloatingView } int directionsToDetectScroll = mSwipeDetector.isIdleState() ? - SwipeDetector.DIRECTION_NEGATIVE : 0; + SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0; mSwipeDetector.setDetectableScrollConditions( directionsToDetectScroll, false); mSwipeDetector.onTouchEvent(ev); @@ -122,7 +124,7 @@ public abstract class AbstractSlideInView extends AbstractFloatingView return mIsOpen && mOpenCloseAnimator.isRunning(); } - /* SwipeDetector.Listener */ + /* SingleAxisSwipeDetector.Listener */ @Override public void onDragStart(boolean start) { } @@ -136,17 +138,17 @@ public abstract class AbstractSlideInView extends AbstractFloatingView } @Override - public void onDragEnd(float velocity, boolean fling) { - if ((fling && velocity > 0) || mTranslationShift > 0.5f) { + public void onDragEnd(float velocity) { + if ((mSwipeDetector.isFling(velocity) && velocity > 0) || mTranslationShift > 0.5f) { mScrollInterpolator = scrollInterpolatorForVelocity(velocity); - mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration( + mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration( velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift)); close(true); } else { mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat( TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED)); mOpenCloseAnimator.setDuration( - SwipeDetector.calculateDuration(velocity, mTranslationShift)) + BaseSwipeDetector.calculateDuration(velocity, mTranslationShift)) .setInterpolator(Interpolators.DEACCEL); mOpenCloseAnimator.start(); } -- cgit v1.2.3