1 package com.android.mail.utils;
2 
3 import android.content.Context;
4 import android.os.SystemClock;
5 
6 import com.google.common.collect.Lists;
7 
8 import 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  */
14 public 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 
InputSmoother(Context context)45     public InputSmoother(Context context) {
46         mDensity = context.getResources().getDisplayMetrics().density;
47     }
48 
onInput(int pos)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      */
getSmoothedVelocity()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