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 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