/* * 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.quickstep.util; import android.content.Context; import android.content.res.Resources; import android.view.MotionEvent; import com.android.launcher3.Alarm; import com.android.launcher3.R; import com.android.launcher3.compat.AccessibilityManagerCompat; /** * Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is * a pause in motion. */ public class MotionPauseDetector { // The percentage of the previous speed that determines whether this is a rapid deceleration. // The bigger this number, the easier it is to trigger the first pause. private static final float RAPID_DECELERATION_FACTOR = 0.6f; /** If no motion is added for this amount of time, assume the motion has paused. */ private static final long FORCE_PAUSE_TIMEOUT = 300; /** * After {@link #makePauseHarderToTrigger()}, must * move slowly for this long to trigger a pause. */ private static final long HARDER_TRIGGER_TIMEOUT = 400; private final float mSpeedVerySlow; private final float mSpeedSlow; private final float mSpeedSomewhatFast; private final float mSpeedFast; private final Alarm mForcePauseTimeout; private final boolean mMakePauseHarderToTrigger; private final Context mContext; private Long mPreviousTime = null; private Float mPreviousPosition = null; private Float mPreviousVelocity = null; private Float mFirstPosition = null; private OnMotionPauseListener mOnMotionPauseListener; private boolean mIsPaused; // Bias more for the first pause to make it feel extra responsive. private boolean mHasEverBeenPaused; /** @see #setDisallowPause(boolean) */ private boolean mDisallowPause; // Time at which speed became < mSpeedSlow (only used if mMakePauseHarderToTrigger == true). private long mSlowStartTime; public MotionPauseDetector(Context context) { this(context, false); } /** * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause. */ public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger) { mContext = context; Resources res = context.getResources(); mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow); mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow); mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast); mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast); mForcePauseTimeout = new Alarm(); mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */)); mMakePauseHarderToTrigger = makePauseHarderToTrigger; } /** * Get callbacks for when motion pauses and resumes. */ public void setOnMotionPauseListener(OnMotionPauseListener listener) { mOnMotionPauseListener = listener; } /** * @param disallowPause If true, we will not detect any pauses until this is set to false again. */ public void setDisallowPause(boolean disallowPause) { mDisallowPause = disallowPause; updatePaused(mIsPaused); } /** * Computes velocity and acceleration to determine whether the motion is paused. * @param position The x or y component of the motion being tracked. * * TODO: Use historical positions as well, e.g. {@link MotionEvent#getHistoricalY(int, int)}. */ public void addPosition(float position, long time) { if (mFirstPosition == null) { mFirstPosition = position; } mForcePauseTimeout.setAlarm(mMakePauseHarderToTrigger ? HARDER_TRIGGER_TIMEOUT : FORCE_PAUSE_TIMEOUT); if (mPreviousTime != null && mPreviousPosition != null) { long changeInTime = Math.max(1, time - mPreviousTime); float changeInPosition = position - mPreviousPosition; float velocity = changeInPosition / changeInTime; if (mPreviousVelocity != null) { checkMotionPaused(velocity, mPreviousVelocity, time); } mPreviousVelocity = velocity; } mPreviousTime = time; mPreviousPosition = position; } private void checkMotionPaused(float velocity, float prevVelocity, long time) { float speed = Math.abs(velocity); float previousSpeed = Math.abs(prevVelocity); boolean isPaused; if (mIsPaused) { // Continue to be paused until moving at a fast speed. isPaused = speed < mSpeedFast || previousSpeed < mSpeedFast; } else { if (velocity < 0 != prevVelocity < 0) { // We're just changing directions, not necessarily stopping. isPaused = false; } else { isPaused = speed < mSpeedVerySlow && previousSpeed < mSpeedVerySlow; if (!isPaused && !mHasEverBeenPaused) { // We want to be more aggressive about detecting the first pause to ensure it // feels as responsive as possible; getting two very slow speeds back to back // takes too long, so also check for a rapid deceleration. boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR; isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast; } if (mMakePauseHarderToTrigger) { if (speed < mSpeedSlow) { if (mSlowStartTime == 0) { mSlowStartTime = time; } isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT; } else { mSlowStartTime = 0; isPaused = false; } } } } updatePaused(isPaused); } private void updatePaused(boolean isPaused) { if (mDisallowPause) { isPaused = false; } if (mIsPaused != isPaused) { mIsPaused = isPaused; if (mIsPaused) { AccessibilityManagerCompat.sendPauseDetectedEventToTest(mContext); mHasEverBeenPaused = true; } if (mOnMotionPauseListener != null) { mOnMotionPauseListener.onMotionPauseChanged(mIsPaused); } } } public void clear() { mPreviousTime = null; mPreviousPosition = null; mPreviousVelocity = null; mFirstPosition = null; setOnMotionPauseListener(null); mIsPaused = mHasEverBeenPaused = false; mSlowStartTime = 0; mForcePauseTimeout.cancelAlarm(); } public boolean isPaused() { return mIsPaused; } public interface OnMotionPauseListener { void onMotionPauseChanged(boolean isPaused); } }