All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowVelocityTracker Maven / Gradle / Ivy

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;

/**
 * Shadow for {@link android.view.VelocityTracker}.
 */
@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 computedVelocityX = new SparseArray<>();
  private SparseArray 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 x = new SparseArray<>();
    public SparseArray 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;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy