blob: aed29a460e1069b9cb05a17bb8f84dacd17d3b8f [file] [log] [blame]
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
protected void clear() {
maybeInitialize();
curIndex = 0;
computedVelocityX.clear();
computedVelocityY.clear();
for (Movement movement : movements) {
movement.clear();
}
}
@Implementation
protected void addMovement(MotionEvent event) {
maybeInitialize();
if (event == null) {
throw new IllegalArgumentException("event must not be null");
}
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
clear();
} else if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
// only listen for DOWN and MOVE events
return;
}
curIndex = (curIndex + 1) % HISTORY_SIZE;
movements[curIndex].set(event);
}
@Implementation
protected void computeCurrentVelocity(int units) {
computeCurrentVelocity(units, Float.MAX_VALUE);
}
@Implementation
protected 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
protected float getXVelocity() {
return getXVelocity(ACTIVE_POINTER_ID);
}
@Implementation
protected float getYVelocity() {
return getYVelocity(ACTIVE_POINTER_ID);
}
@Implementation
protected float getXVelocity(int id) {
if (id == ACTIVE_POINTER_ID) {
id = activePointerId;
}
return computedVelocityX.get(id, 0f);
}
@Implementation
protected 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;
}
}
}