package org.robolectric.shadows; import android.util.SparseArray; import android.view.MotionEvent; import android.view.VelocityTracker; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @Implements(VelocityTracker.class) public class ShadowVelocityTracker { private static final int ACTIVE_POINTER_ID = -1; private static final int HISTORY_SIZE = 20; private static final long HORIZON_MS = 200L; private static final long MIN_DURATION = 10L; private boolean initialized = false; private int activePointerId = -1; private final Movement[] movements = new Movement[HISTORY_SIZE]; private int curIndex = 0; private SparseArray<Float> computedVelocityX = new SparseArray<>(); private SparseArray<Float> computedVelocityY = new SparseArray<>(); private void maybeInitialize() { if (initialized) { return; } for (int i = 0; i < movements.length; i++) { movements[i] = new Movement(); } initialized = true; } @Implementation public void clear() { maybeInitialize(); curIndex = 0; computedVelocityX.clear(); computedVelocityY.clear(); for (Movement movement : movements) { movement.clear(); } } @Implementation public void addMovement(MotionEvent event) { maybeInitialize(); if (event == null) { throw new IllegalArgumentException("event must not be null"); } if (event.getAction() == MotionEvent.ACTION_DOWN) { clear(); } else if (event.getAction() != MotionEvent.ACTION_MOVE) { // only listen for DOWN and MOVE events return; } curIndex = (curIndex + 1) % HISTORY_SIZE; movements[curIndex].set(event); } @Implementation public void computeCurrentVelocity(int units) { computeCurrentVelocity(units, Float.MAX_VALUE); } @Implementation public void computeCurrentVelocity(int units, float maxVelocity) { maybeInitialize(); // Estimation based on AOSP's LegacyVelocityTrackerStrategy Movement newestMovement = movements[curIndex]; if (!newestMovement.isSet()) { // no movements added, so we can assume that the current velocity is 0 (and already set that // way) return; } for (int pointerId : newestMovement.pointerIds) { // Find the oldest sample that is for the same pointer, but not older than HORIZON_MS long minTime = newestMovement.eventTime - HORIZON_MS; int oldestIndex = curIndex; int numTouches = 1; do { int nextOldestIndex = (oldestIndex == 0 ? HISTORY_SIZE : oldestIndex) - 1; Movement nextOldestMovement = movements[nextOldestIndex]; if (!nextOldestMovement.hasPointer(pointerId) || nextOldestMovement.eventTime < minTime) { break; } oldestIndex = nextOldestIndex; } while (++numTouches < HISTORY_SIZE); float accumVx = 0f; float accumVy = 0f; int index = oldestIndex; Movement oldestMovement = movements[oldestIndex]; long lastDuration = 0; while (numTouches-- > 1) { if (++index == HISTORY_SIZE) { index = 0; } Movement movement = movements[index]; long duration = movement.eventTime - oldestMovement.eventTime; if (duration >= MIN_DURATION) { float scale = 1000f / duration; // one over time delta in seconds float vx = (movement.x.get(pointerId) - oldestMovement.x.get(pointerId)) * scale; float vy = (movement.y.get(pointerId) - oldestMovement.y.get(pointerId)) * scale; accumVx = (accumVx * lastDuration + vx * duration) / (duration + lastDuration); accumVy = (accumVy * lastDuration + vy * duration) / (duration + lastDuration); lastDuration = duration; } } computedVelocityX.put(pointerId, windowed(accumVx * units / 1000, maxVelocity)); computedVelocityY.put(pointerId, windowed(accumVy * units / 1000, maxVelocity)); } activePointerId = newestMovement.activePointerId; } private float windowed(float value, float max) { return Math.min(max, Math.max(-max, value)); } @Implementation public float getXVelocity() { return getXVelocity(ACTIVE_POINTER_ID); } @Implementation public float getYVelocity() { return getYVelocity(ACTIVE_POINTER_ID); } @Implementation public float getXVelocity(int id) { if (id == ACTIVE_POINTER_ID) { id = activePointerId; } return computedVelocityX.get(id, 0f); } @Implementation public float getYVelocity(int id) { if (id == ACTIVE_POINTER_ID) { id = activePointerId; } return computedVelocityY.get(id, 0f); } private static class Movement { public int pointerCount = 0; public int[] pointerIds = new int[0]; public int activePointerId = -1; public long eventTime; public SparseArray<Float> x = new SparseArray<>(); public SparseArray<Float> y = new SparseArray<>(); public void set(MotionEvent event) { pointerCount = event.getPointerCount(); pointerIds = new int[pointerCount]; x.clear(); y.clear(); for (int i = 0; i < pointerCount; i++) { pointerIds[i] = event.getPointerId(i); x.put(pointerIds[i], event.getX(i)); y.put(pointerIds[i], event.getY(i)); } activePointerId = event.getPointerId(0); eventTime = event.getEventTime(); } public void clear() { pointerCount = 0; activePointerId = -1; } public boolean isSet() { return pointerCount != 0; } public boolean hasPointer(int pointerId) { return x.get(pointerId) != null; } } }