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