summaryrefslogtreecommitdiffstats
path: root/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
blob: ae5f3908be7de6be611f8f1b7989e7b7d4c63c19 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/*
 * 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;

/**
 * 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;

    private final float mSpeedVerySlow;
    private final float mSpeedSomewhatFast;
    private final float mSpeedFast;
    private final float mMinDisplacementForPause;
    private final Alarm mForcePauseTimeout;

    private Long mPreviousTime = null;
    private Float mPreviousPosition = null;
    private Float mPreviousVelocity = null;

    private TotalDisplacement mTotalDisplacement = new TotalDisplacement();
    private Float mFirstPosition = null;
    private Float mFirstOrthogonalPosition = null;

    private OnMotionPauseListener mOnMotionPauseListener;
    private boolean mIsPaused;
    // Bias more for the first pause to make it feel extra responsive.
    private boolean mHasEverBeenPaused;

    public MotionPauseDetector(Context context) {
        Resources res = context.getResources();
        mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
        mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
        mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
        mMinDisplacementForPause = res.getDimension(R.dimen.motion_pause_detector_min_displacement);
        mForcePauseTimeout = new Alarm();
        mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
    }

    /**
     * Get callbacks for when motion pauses and resumes, including an
     * immediate callback with the current pause state.
     */
    public void setOnMotionPauseListener(OnMotionPauseListener listener) {
        mOnMotionPauseListener = listener;
    }

    /**
     * Computes velocity and acceleration to determine whether the motion is paused.
     * @param position The x or y component of the motion being tracked.
     * @param orthogonalPosition The x or y component (opposite of {@param position}) of the motion.
     *
     * TODO: Use historical positions as well, e.g. {@link MotionEvent#getHistoricalY(int, int)}.
     */
    public void addPosition(float position, float orthogonalPosition, long time) {
        if (mFirstPosition == null) {
            mFirstPosition = position;
        }
        if (mFirstOrthogonalPosition == null) {
            mFirstOrthogonalPosition = orthogonalPosition;
        }
        mForcePauseTimeout.setAlarm(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) {
                mTotalDisplacement.set(Math.abs(position - mFirstPosition),
                        Math.abs(orthogonalPosition - mFirstOrthogonalPosition));
                checkMotionPaused(velocity, mPreviousVelocity, mTotalDisplacement);
            }
            mPreviousVelocity = velocity;
        }
        mPreviousTime = time;
        mPreviousPosition = position;
    }

    private void checkMotionPaused(float velocity, float prevVelocity,
            TotalDisplacement totalDisplacement) {
        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;
                }
            }
        }
        boolean passedMinDisplacement = totalDisplacement.primary >= mMinDisplacementForPause;
        boolean isDisplacementOrthogonal = totalDisplacement.orthogonal > totalDisplacement.primary;
        if (!passedMinDisplacement || isDisplacementOrthogonal) {
            mForcePauseTimeout.cancelAlarm();
            isPaused = false;
        }
        updatePaused(isPaused);
    }

    private void updatePaused(boolean isPaused) {
        if (mIsPaused != isPaused) {
            mIsPaused = isPaused;
            if (mIsPaused) {
                mHasEverBeenPaused = true;
            }
            if (mOnMotionPauseListener != null) {
                mOnMotionPauseListener.onMotionPauseChanged(mIsPaused);
            }
        }
    }

    public void clear() {
        mPreviousTime = null;
        mPreviousPosition = null;
        mPreviousVelocity = null;
        mFirstPosition = null;
        mFirstOrthogonalPosition = null;
        mTotalDisplacement.set(0, 0);
        setOnMotionPauseListener(null);
        mIsPaused = mHasEverBeenPaused = false;
        mForcePauseTimeout.cancelAlarm();
    }

    public boolean isPaused() {
        return mIsPaused;
    }

    public interface OnMotionPauseListener {
        void onMotionPauseChanged(boolean isPaused);
    }

    /**
     * Contains the displacement from the first tracked position,
     * along both the primary and orthogonal axes.
     */
    private class TotalDisplacement {
        public float primary;
        public float orthogonal;

        public void set(float primaryDisplacement, float orthogonalDisplacement) {
            this.primary = primaryDisplacement;
            this.orthogonal = orthogonalDisplacement;
        }
    }
}