1package com.android.mail.utils;
2
3import android.content.Context;
4import android.os.SystemClock;
5
6import com.google.common.collect.Lists;
7
8import java.util.Deque;
9
10/**
11 * Utility class to calculate a velocity using a moving average filter of recent input positions.
12 * Intended to smooth out touch input events.
13 */
14public class InputSmoother {
15
16    /**
17     * Some devices have significant sampling noise: it could be that samples come in too late,
18     * or that the reported position doesn't quite match up with the time. Instantaneous velocity
19     * on these devices is too jittery to be useful in deciding whether to instantly snap, so smooth
20     * out the data using a moving average over this window size. A sample window size n will
21     * effectively average the velocity over n-1 points, so n=2 is the minimum valid value (no
22     * averaging at all).
23     */
24    private static final int SAMPLING_WINDOW_SIZE = 5;
25
26    /**
27     * The maximum elapsed time (in millis) between samples that we would consider "consecutive".
28     * Only consecutive samples will factor into the rolling average sample window.
29     * Any samples that are older than this maximum are continually purged from the sample window,
30     * so as to avoid skewing the average with irrelevant older values.
31     */
32    private static final long MAX_SAMPLE_INTERVAL_MS = 200;
33
34    /**
35     * Sampling window to calculate rolling average of scroll velocity.
36     */
37    private final Deque<Sample> mRecentSamples = Lists.newLinkedList();
38    private final float mDensity;
39
40    private static class Sample {
41        int pos;
42        long millis;
43    }
44
45    public InputSmoother(Context context) {
46        mDensity = context.getResources().getDisplayMetrics().density;
47    }
48
49    public void onInput(int pos) {
50        Sample sample;
51        final long nowMs = SystemClock.uptimeMillis();
52
53        final Sample last = mRecentSamples.peekLast();
54        if (last != null && nowMs - last.millis > MAX_SAMPLE_INTERVAL_MS) {
55            mRecentSamples.clear();
56        }
57
58        if (mRecentSamples.size() == SAMPLING_WINDOW_SIZE) {
59            sample = mRecentSamples.removeFirst();
60        } else {
61            sample = new Sample();
62        }
63        sample.pos = pos;
64        sample.millis = nowMs;
65
66        mRecentSamples.add(sample);
67    }
68
69    /**
70     * Calculates velocity based on recent inputs from {@link #onInput(int)}, averaged together to
71     * smooth out jitter.
72     *
73     * @return returns velocity in dp/s, or null if not enough samples have been collected
74     */
75    public Float getSmoothedVelocity() {
76        if (mRecentSamples.size() < 2) {
77            // need at least 2 position samples to determine a velocity
78            return null;
79        }
80
81        // calculate moving average over current window
82        int totalDistancePx = 0;
83        int prevPos = mRecentSamples.getFirst().pos;
84        final long totalTimeMs = mRecentSamples.getLast().millis - mRecentSamples.getFirst().millis;
85
86        if (totalTimeMs <= 0) {
87            // samples are really fast or bad. no answer.
88            return null;
89        }
90
91        for (Sample s : mRecentSamples) {
92            totalDistancePx += Math.abs(s.pos - prevPos);
93            prevPos = s.pos;
94        }
95        final float distanceDp = totalDistancePx / mDensity;
96        // velocity in dp per second
97        return distanceDp * 1000 / totalTimeMs;
98    }
99
100}
101