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 --- .../FlingAndHoldTouchController.java | 4 +- .../NavBarToHomeTouchController.java | 16 +- .../QuickSwitchTouchController.java | 6 +- .../touchcontrollers/TaskViewTouchController.java | 30 +- .../TransposedQuickSwitchTouchController.java | 4 +- .../LandscapeEdgeSwipeController.java | 4 +- .../PortraitStatesTouchController.java | 4 +- .../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 +- .../uioverrides/AllAppsSwipeController.java | 4 +- .../touch/SingleAxisSwipeDetectorTest.java | 171 +++++++++ .../android/launcher3/touch/SwipeDetectorTest.java | 165 --------- 17 files changed, 705 insertions(+), 622 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 create mode 100644 tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java delete mode 100644 tests/src/com/android/launcher3/touch/SwipeDetectorTest.java diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java index e215cfe8d..ad16e56cd 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java @@ -173,7 +173,7 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController { } @Override - public void onDragEnd(float velocity, boolean fling) { + public void onDragEnd(float velocity) { if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) { if (mPeekAnim != null) { mPeekAnim.cancel(); @@ -196,7 +196,7 @@ public class FlingAndHoldTouchController extends PortraitStatesTouchController { }); overviewAnim.start(); } else { - super.onDragEnd(velocity, fling); + super.onDragEnd(velocity); } View searchView = mLauncher.getAppsView().getSearchView(); diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java index d66af1ae2..738436a75 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java @@ -43,7 +43,7 @@ import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.AnimatorSetBuilder; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.compat.AccessibilityManagerCompat; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.util.TouchController; @@ -52,12 +52,13 @@ import com.android.quickstep.views.RecentsView; /** * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps. */ -public class NavBarToHomeTouchController implements TouchController, SwipeDetector.Listener { +public class NavBarToHomeTouchController implements TouchController, + SingleAxisSwipeDetector.Listener { private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3; private final Launcher mLauncher; - private final SwipeDetector mSwipeDetector; + private final SingleAxisSwipeDetector mSwipeDetector; private final float mPullbackDistance; private boolean mNoIntercept; @@ -67,7 +68,8 @@ public class NavBarToHomeTouchController implements TouchController, SwipeDetect public NavBarToHomeTouchController(Launcher launcher) { mLauncher = launcher; - mSwipeDetector = new SwipeDetector(mLauncher, this, SwipeDetector.VERTICAL); + mSwipeDetector = new SingleAxisSwipeDetector(mLauncher, this, + SingleAxisSwipeDetector.VERTICAL); mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance); } @@ -79,7 +81,8 @@ public class NavBarToHomeTouchController implements TouchController, SwipeDetect if (mNoIntercept) { return false; } - mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false); + mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_POSITIVE, + false /* ignoreSlop */); } if (mNoIntercept) { @@ -173,7 +176,8 @@ public class NavBarToHomeTouchController implements TouchController, SwipeDetect } @Override - public void onDragEnd(float velocity, boolean fling) { + public void onDragEnd(float velocity) { + boolean fling = mSwipeDetector.isFling(velocity); final int logAction = fling ? Touch.FLING : Touch.SWIPE; float progress = mCurrentAnimation.getProgressFraction(); float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(progress); diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java index 6576ec573..379f41c53 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java @@ -42,7 +42,7 @@ import com.android.launcher3.LauncherStateManager; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorSetBuilder; import com.android.launcher3.touch.AbstractStateChangeTouchController; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.quickstep.OverviewInteractionState; @@ -59,10 +59,10 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll private @Nullable TaskView mTaskToLaunch; public QuickSwitchTouchController(Launcher launcher) { - this(launcher, SwipeDetector.HORIZONTAL); + this(launcher, SingleAxisSwipeDetector.HORIZONTAL); } - protected QuickSwitchTouchController(Launcher l, SwipeDetector.Direction dir) { + protected QuickSwitchTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) { super(l, dir); } diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java index 00e4f58e9..ad02de109 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java @@ -19,6 +19,9 @@ import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; +import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH; +import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE; +import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE; import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs; import android.animation.Animator; @@ -32,7 +35,8 @@ import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.BaseSwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.util.FlingBlockCheck; import com.android.launcher3.util.PendingAnimation; @@ -46,15 +50,14 @@ import com.android.quickstep.views.TaskView; * Touch controller for handling task view card swipes */ public abstract class TaskViewTouchController - extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener { - - private static final String TAG = "OverviewSwipeController"; + extends AnimatorListenerAdapter 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; protected final T mActivity; - private final SwipeDetector mDetector; + private final SingleAxisSwipeDetector mDetector; private final RecentsView mRecentsView; private final int[] mTempCords = new int[2]; @@ -74,7 +77,7 @@ public abstract class TaskViewTouchController public TaskViewTouchController(T activity) { mActivity = activity; mRecentsView = activity.getOverviewPanel(); - mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL); + mDetector = new SingleAxisSwipeDetector(activity, this, SingleAxisSwipeDetector.VERTICAL); } private boolean canInterceptTouch() { @@ -113,7 +116,7 @@ public abstract class TaskViewTouchController int directionsToDetectScroll = 0; boolean ignoreSlopWhenSettling = false; if (mCurrentAnimation != null) { - directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; + directionsToDetectScroll = DIRECTION_BOTH; ignoreSlopWhenSettling = true; } else { mTaskBeingDragged = null; @@ -126,12 +129,12 @@ public abstract class TaskViewTouchController if (!SysUINavigationMode.getMode(mActivity).hasGestures) { // Don't allow swipe down to open if we don't support swipe up // to enter overview. - directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; + directionsToDetectScroll = DIRECTION_POSITIVE; } else { // The task can be dragged up to dismiss it, // and down to open if it's the current page. directionsToDetectScroll = i == mRecentsView.getCurrentPage() - ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE; + ? DIRECTION_BOTH : DIRECTION_POSITIVE; } break; } @@ -165,8 +168,8 @@ public abstract class TaskViewTouchController return; } int scrollDirections = mDetector.getScrollDirections(); - if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0) - || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) { + if (goingUp && ((scrollDirections & DIRECTION_POSITIVE) == 0) + || !goingUp && ((scrollDirections & DIRECTION_NEGATIVE) == 0)) { // Trying to re-init in an unsupported direction. return; } @@ -243,7 +246,8 @@ public abstract class TaskViewTouchController } @Override - public void onDragEnd(float velocity, boolean fling) { + public void onDragEnd(float velocity) { + boolean fling = mDetector.isFling(velocity); final boolean goingToEnd; final int logAction; boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); @@ -260,7 +264,7 @@ public abstract class TaskViewTouchController logAction = Touch.SWIPE; goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS; } - long animationDuration = SwipeDetector.calculateDuration( + long animationDuration = BaseSwipeDetector.calculateDuration( velocity, goingToEnd ? (1 - progress) : progress); if (blockedFling && !goingToEnd) { animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java index f1e4041eb..0ed529184 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java @@ -17,12 +17,12 @@ package com.android.launcher3.uioverrides.touchcontrollers; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; public class TransposedQuickSwitchTouchController extends QuickSwitchTouchController { public TransposedQuickSwitchTouchController(Launcher launcher) { - super(launcher, SwipeDetector.VERTICAL); + super(launcher, SingleAxisSwipeDetector.VERTICAL); } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java index bb72315d3..39b0f8d21 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java @@ -11,7 +11,7 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherStateManager.AnimationComponents; import com.android.launcher3.touch.AbstractStateChangeTouchController; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.quickstep.RecentsModel; @@ -24,7 +24,7 @@ public class LandscapeEdgeSwipeController extends AbstractStateChangeTouchContro private static final String TAG = "LandscapeEdgeSwipeCtrl"; public LandscapeEdgeSwipeController(Launcher l) { - super(l, SwipeDetector.HORIZONTAL); + super(l, SingleAxisSwipeDetector.HORIZONTAL); } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java index 981329556..db6a40f2e 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java @@ -43,7 +43,7 @@ import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.AnimatorSetBuilder; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.touch.AbstractStateChangeTouchController; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.uioverrides.states.OverviewState; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; @@ -79,7 +79,7 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr private boolean mFinishFastOnSecondTouch; public PortraitStatesTouchController(Launcher l, boolean allowDragToOverview) { - super(l, SwipeDetector.VERTICAL); + super(l, SingleAxisSwipeDetector.VERTICAL); mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l); mAllowDragToOverview = allowDragToOverview; } 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(); } diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java index bd6ea502e..23f21a1cb 100644 --- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java +++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java @@ -10,7 +10,7 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherStateManager.AnimationComponents; import com.android.launcher3.touch.AbstractStateChangeTouchController; -import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; /** @@ -21,7 +21,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController { private MotionEvent mTouchDownEvent; public AllAppsSwipeController(Launcher l) { - super(l, SwipeDetector.VERTICAL); + super(l, SingleAxisSwipeDetector.VERTICAL); } @Override diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java new file mode 100644 index 000000000..5174e4dab --- /dev/null +++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java @@ -0,0 +1,171 @@ +/* + * 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 com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH; +import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE; +import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE; +import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL; +import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL; + +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +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; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SingleAxisSwipeDetectorTest { + + private static final String TAG = SingleAxisSwipeDetectorTest.class.getSimpleName(); + public static void L(String s, Object... parts) { + Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts)); + } + + private TouchEventGenerator mGenerator; + private SingleAxisSwipeDetector mDetector; + private int mTouchSlop; + + @Mock + private SingleAxisSwipeDetector.Listener mMockListener; + + @Mock + private ViewConfiguration mMockConfig; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mGenerator = new TouchEventGenerator((ev) -> mDetector.onTouchEvent(ev)); + ViewConfiguration orgConfig = ViewConfiguration + .get(InstrumentationRegistry.getTargetContext()); + doReturn(orgConfig.getScaledMaximumFlingVelocity()).when(mMockConfig) + .getScaledMaximumFlingVelocity(); + + mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false); + mDetector.setDetectableScrollConditions(DIRECTION_BOTH, false); + mTouchSlop = orgConfig.getScaledTouchSlop(); + doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop(); + + L("mTouchSlop=", mTouchSlop); + } + + @Test + public void testDragStart_verticalPositive() { + mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false); + mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false); + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100, 100 - mTouchSlop); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragStart(anyBoolean()); + } + + @Test + public void testDragStart_verticalNegative() { + mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false); + mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false); + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100, 100 + mTouchSlop); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragStart(anyBoolean()); + } + + @Test + public void testDragStart_failed() { + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100 + mTouchSlop, 100); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener, never()).onDragStart(anyBoolean()); + } + + @Test + public void testDragStart_horizontalPositive() { + mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false); + mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false); + + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100 + mTouchSlop, 100); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragStart(anyBoolean()); + } + + @Test + public void testDragStart_horizontalNegative() { + mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false); + mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false); + + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100 - mTouchSlop, 100); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragStart(anyBoolean()); + } + + @Test + public void testDragStart_horizontalRtlPositive() { + mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true); + mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false); + + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100 - mTouchSlop, 100); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragStart(anyBoolean()); + } + + @Test + public void testDragStart_horizontalRtlNegative() { + mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true); + mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false); + + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100 + mTouchSlop, 100); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragStart(anyBoolean()); + } + + @Test + public void testDrag() { + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100, 100 + mTouchSlop); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDrag(anyFloat(), anyObject()); + } + + @Test + public void testDragEnd() { + mGenerator.put(0, 100, 100); + mGenerator.move(0, 100, 100 + mTouchSlop); + mGenerator.move(0, 100, 100 + mTouchSlop * 2); + mGenerator.lift(0); + // TODO: actually calculate the following parameters and do exact value checks. + verify(mMockListener).onDragEnd(anyFloat()); + } +} diff --git a/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java deleted file mode 100644 index f209fae97..000000000 --- a/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java +++ /dev/null @@ -1,165 +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 org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyFloat; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -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; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class SwipeDetectorTest { - - private static final String TAG = SwipeDetectorTest.class.getSimpleName(); - public static void L(String s, Object... parts) { - Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts)); - } - - private TouchEventGenerator mGenerator; - private SwipeDetector mDetector; - private int mTouchSlop; - - @Mock - private SwipeDetector.Listener mMockListener; - - @Mock - private ViewConfiguration mMockConfig; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mGenerator = new TouchEventGenerator((ev) -> mDetector.onTouchEvent(ev)); - ViewConfiguration orgConfig = ViewConfiguration - .get(InstrumentationRegistry.getTargetContext()); - doReturn(orgConfig.getScaledMaximumFlingVelocity()).when(mMockConfig) - .getScaledMaximumFlingVelocity(); - - mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false); - mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false); - mTouchSlop = orgConfig.getScaledTouchSlop(); - doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop(); - - L("mTouchSlop=", mTouchSlop); - } - - @Test - public void testDragStart_verticalPositive() { - mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false); - mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false); - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100, 100 - mTouchSlop); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener).onDragStart(anyBoolean()); - } - - @Test - public void testDragStart_verticalNegative() { - mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false); - mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false); - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100, 100 + mTouchSlop); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener).onDragStart(anyBoolean()); - } - - @Test - public void testDragStart_failed() { - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100 + mTouchSlop, 100); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener, never()).onDragStart(anyBoolean()); - } - - @Test - public void testDragStart_horizontalPositive() { - mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false); - mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false); - - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100 + mTouchSlop, 100); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener).onDragStart(anyBoolean()); - } - - @Test - public void testDragStart_horizontalNegative() { - mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false); - mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false); - - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100 - mTouchSlop, 100); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener).onDragStart(anyBoolean()); - } - - @Test - public void testDragStart_horizontalRtlPositive() { - mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true); - mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false); - - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100 - mTouchSlop, 100); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener).onDragStart(anyBoolean()); - } - - @Test - public void testDragStart_horizontalRtlNegative() { - mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true); - mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false); - - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100 + mTouchSlop, 100); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener).onDragStart(anyBoolean()); - } - - @Test - public void testDrag() { - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100, 100 + mTouchSlop); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener).onDrag(anyFloat(), anyObject()); - } - - @Test - public void testDragEnd() { - mGenerator.put(0, 100, 100); - mGenerator.move(0, 100, 100 + mTouchSlop); - mGenerator.move(0, 100, 100 + mTouchSlop * 2); - mGenerator.lift(0); - // TODO: actually calculate the following parameters and do exact value checks. - verify(mMockListener).onDragEnd(anyFloat(), anyBoolean()); - } -} -- cgit v1.2.3