Home | History | Annotate | Download | only in shadows
      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 
     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
     36   public 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
     47   public void addMovement(MotionEvent event) {
     48     maybeInitialize();
     49     if (event == null) {
     50       throw new IllegalArgumentException("event must not be null");
     51     }
     52 
     53     if (event.getAction() == MotionEvent.ACTION_DOWN) {
     54       clear();
     55     } else if (event.getAction() != 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
     65   public void computeCurrentVelocity(int units) {
     66     computeCurrentVelocity(units, Float.MAX_VALUE);
     67   }
     68 
     69   @Implementation
     70   public 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 
    127   private float windowed(float value, float max) {
    128     return Math.min(max, Math.max(-max, value));
    129   }
    130 
    131   @Implementation
    132   public float getXVelocity() {
    133     return getXVelocity(ACTIVE_POINTER_ID);
    134   }
    135 
    136   @Implementation
    137   public float getYVelocity() {
    138     return getYVelocity(ACTIVE_POINTER_ID);
    139   }
    140 
    141   @Implementation
    142   public 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
    151   public 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 
    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 
    181     public void clear() {
    182       pointerCount = 0;
    183       activePointerId = -1;
    184     }
    185 
    186     public boolean isSet() {
    187       return pointerCount != 0;
    188     }
    189 
    190     public boolean hasPointer(int pointerId) {
    191       return x.get(pointerId) != null;
    192     }
    193   }
    194 }
    195