1 package org.robolectric.shadows;
2 
3 import android.util.SparseArray;
4 import android.view.MotionEvent;
5 import android.view.VelocityTracker;
6 import org.robolectric.annotation.Implementation;
7 import org.robolectric.annotation.Implements;
8 
9 @Implements(VelocityTracker.class)
10 public class ShadowVelocityTracker {
11   private static final int ACTIVE_POINTER_ID = -1;
12   private static final int HISTORY_SIZE = 20;
13   private static final long HORIZON_MS = 200L;
14   private static final long MIN_DURATION = 10L;
15 
16   private boolean initialized = false;
17   private int activePointerId = -1;
18   private final Movement[] movements = new Movement[HISTORY_SIZE];
19   private int curIndex = 0;
20 
21   private SparseArray<Float> computedVelocityX = new SparseArray<>();
22   private SparseArray<Float> computedVelocityY = new SparseArray<>();
23 
maybeInitialize()24   private void maybeInitialize() {
25     if (initialized) {
26       return;
27     }
28 
29     for (int i = 0; i < movements.length; i++) {
30       movements[i] = new Movement();
31     }
32     initialized = true;
33   }
34 
35   @Implementation
clear()36   protected void clear() {
37     maybeInitialize();
38     curIndex = 0;
39     computedVelocityX.clear();
40     computedVelocityY.clear();
41     for (Movement movement : movements) {
42       movement.clear();
43     }
44   }
45 
46   @Implementation
addMovement(MotionEvent event)47   protected void addMovement(MotionEvent event) {
48     maybeInitialize();
49     if (event == null) {
50       throw new IllegalArgumentException("event must not be null");
51     }
52 
53     if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
54       clear();
55     } else if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
56       // only listen for DOWN and MOVE events
57       return;
58     }
59 
60     curIndex = (curIndex + 1) % HISTORY_SIZE;
61     movements[curIndex].set(event);
62   }
63 
64   @Implementation
computeCurrentVelocity(int units)65   protected void computeCurrentVelocity(int units) {
66     computeCurrentVelocity(units, Float.MAX_VALUE);
67   }
68 
69   @Implementation
computeCurrentVelocity(int units, float maxVelocity)70   protected void computeCurrentVelocity(int units, float maxVelocity) {
71     maybeInitialize();
72 
73     // Estimation based on AOSP's LegacyVelocityTrackerStrategy
74     Movement newestMovement = movements[curIndex];
75     if (!newestMovement.isSet()) {
76       // no movements added, so we can assume that the current velocity is 0 (and already set that
77       // way)
78       return;
79     }
80 
81     for (int pointerId : newestMovement.pointerIds) {
82       // Find the oldest sample that is for the same pointer, but not older than HORIZON_MS
83       long minTime = newestMovement.eventTime - HORIZON_MS;
84       int oldestIndex = curIndex;
85       int numTouches = 1;
86       do {
87         int nextOldestIndex = (oldestIndex == 0 ? HISTORY_SIZE : oldestIndex) - 1;
88         Movement nextOldestMovement = movements[nextOldestIndex];
89         if (!nextOldestMovement.hasPointer(pointerId) || nextOldestMovement.eventTime < minTime) {
90           break;
91         }
92 
93         oldestIndex = nextOldestIndex;
94       } while (++numTouches < HISTORY_SIZE);
95 
96       float accumVx = 0f;
97       float accumVy = 0f;
98       int index = oldestIndex;
99       Movement oldestMovement = movements[oldestIndex];
100       long lastDuration = 0;
101 
102       while (numTouches-- > 1) {
103         if (++index == HISTORY_SIZE) {
104           index = 0;
105         }
106 
107         Movement movement = movements[index];
108         long duration = movement.eventTime - oldestMovement.eventTime;
109 
110         if (duration >= MIN_DURATION) {
111           float scale = 1000f / duration; // one over time delta in seconds
112           float vx = (movement.x.get(pointerId) - oldestMovement.x.get(pointerId)) * scale;
113           float vy = (movement.y.get(pointerId) - oldestMovement.y.get(pointerId)) * scale;
114           accumVx = (accumVx * lastDuration + vx * duration) / (duration + lastDuration);
115           accumVy = (accumVy * lastDuration + vy * duration) / (duration + lastDuration);
116           lastDuration = duration;
117         }
118       }
119 
120       computedVelocityX.put(pointerId, windowed(accumVx * units / 1000, maxVelocity));
121       computedVelocityY.put(pointerId, windowed(accumVy * units / 1000, maxVelocity));
122     }
123 
124     activePointerId = newestMovement.activePointerId;
125   }
126 
windowed(float value, float max)127   private float windowed(float value, float max) {
128     return Math.min(max, Math.max(-max, value));
129   }
130 
131   @Implementation
getXVelocity()132   protected float getXVelocity() {
133     return getXVelocity(ACTIVE_POINTER_ID);
134   }
135 
136   @Implementation
getYVelocity()137   protected float getYVelocity() {
138     return getYVelocity(ACTIVE_POINTER_ID);
139   }
140 
141   @Implementation
getXVelocity(int id)142   protected float getXVelocity(int id) {
143     if (id == ACTIVE_POINTER_ID) {
144       id = activePointerId;
145     }
146 
147     return computedVelocityX.get(id, 0f);
148   }
149 
150   @Implementation
getYVelocity(int id)151   protected float getYVelocity(int id) {
152     if (id == ACTIVE_POINTER_ID) {
153       id = activePointerId;
154     }
155 
156     return computedVelocityY.get(id, 0f);
157   }
158 
159   private static class Movement {
160     public int pointerCount = 0;
161     public int[] pointerIds = new int[0];
162     public int activePointerId = -1;
163     public long eventTime;
164     public SparseArray<Float> x = new SparseArray<>();
165     public SparseArray<Float> y = new SparseArray<>();
166 
set(MotionEvent event)167     public void set(MotionEvent event) {
168       pointerCount = event.getPointerCount();
169       pointerIds = new int[pointerCount];
170       x.clear();
171       y.clear();
172       for (int i = 0; i < pointerCount; i++) {
173         pointerIds[i] = event.getPointerId(i);
174         x.put(pointerIds[i], event.getX(i));
175         y.put(pointerIds[i], event.getY(i));
176       }
177       activePointerId = event.getPointerId(0);
178       eventTime = event.getEventTime();
179     }
180 
clear()181     public void clear() {
182       pointerCount = 0;
183       activePointerId = -1;
184     }
185 
isSet()186     public boolean isSet() {
187       return pointerCount != 0;
188     }
189 
hasPointer(int pointerId)190     public boolean hasPointer(int pointerId) {
191       return x.get(pointerId) != null;
192     }
193   }
194 }
195