Determine prediction amount dynamically
This change adds a utility to calculate th right amount of prediction
needed for the device.
Bug: 232941452
Test: tested on sample app, confirmed that prediction works as expected
Change-Id: Ifc4fe25a08479873e15ef0d8793bb31dc0d683a4
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
index d51dbf4..66da8cf 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
@@ -65,6 +65,6 @@
* @return the new predictor instance
*/
static @NonNull MotionEventPredictor newInstance(@NonNull View view) {
- return new KalmanMotionEventPredictor();
+ return new KalmanMotionEventPredictor(view.getContext());
}
}
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
index 7a7ab0f..9a41bb5 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
@@ -18,20 +18,24 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import android.content.Context;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.input.motionprediction.MotionEventPredictor;
+import androidx.input.motionprediction.utils.PredictionEstimator;
/**
*/
@RestrictTo(LIBRARY)
public class KalmanMotionEventPredictor implements MotionEventPredictor {
private MultiPointerPredictor mMultiPointerPredictor = new MultiPointerPredictor();
+ private PredictionEstimator mPredictionEstimator;
- public KalmanMotionEventPredictor() {
+ public KalmanMotionEventPredictor(@NonNull Context context) {
+ mPredictionEstimator = new PredictionEstimator(context);
}
@Override
@@ -39,6 +43,7 @@
if (mMultiPointerPredictor == null) {
return;
}
+ mPredictionEstimator.record(event);
mMultiPointerPredictor.onTouchEvent(event);
}
@@ -48,7 +53,8 @@
if (mMultiPointerPredictor == null) {
return null;
}
- return mMultiPointerPredictor.predict(1);
+ final int predictionTimeDelta = mPredictionEstimator.estimate();
+ return mMultiPointerPredictor.predict(predictionTimeDelta);
}
@Override
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java
new file mode 100644
index 0000000..3ed9a1b
--- /dev/null
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.input.motionprediction.utils;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.SystemClock;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ */
+@SuppressWarnings("deprecation")
+@RestrictTo(LIBRARY)
+public class PredictionEstimator {
+ private static final int MAX_PREDICTION_MS = 32;
+ private static final int LEGACY_FRAME_TIME_MS = 16;
+ private static final int MS_IN_A_SECOND = 1000;
+
+ private long mLastEventTime = -1;
+ private final float mFrameTimeMs;
+
+ public PredictionEstimator(@NonNull Context context) {
+ mFrameTimeMs = getFastestFrameTimeMs(context);
+ }
+
+ /** Records the needed information from the event to calculate the prediction. */
+ public void record(@NonNull MotionEvent event) {
+ mLastEventTime = event.getEventTime();
+ }
+
+ /** Return the estimated amount of prediction needed. */
+ public int estimate() {
+ if (mLastEventTime <= 0) {
+ return (int) mFrameTimeMs;
+ }
+ // The amount of prediction is the estimated amount of time it will take to land the
+ // information on the screen from now, plus the time since the last recorded MotionEvent
+ int estimatedMs = (int) (SystemClock.uptimeMillis() - mLastEventTime + mFrameTimeMs);
+ return Math.min(MAX_PREDICTION_MS, estimatedMs);
+ }
+
+ private Display getDisplayForContext(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return Api30Impl.getDisplayForContext(context);
+ }
+ return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay();
+ }
+
+ private float getFastestFrameTimeMs(Context context) {
+ Display defaultDisplay = getDisplayForContext(context);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return Api23Impl.getFastestFrameTimeMs(defaultDisplay);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return Api21Impl.getFastestFrameTimeMs(defaultDisplay);
+ } else {
+ return LEGACY_FRAME_TIME_MS;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ static class Api21Impl {
+ private Api21Impl() {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static float getFastestFrameTimeMs(Display display) {
+ float[] refreshRates = display.getSupportedRefreshRates();
+ float largestRefreshRate = refreshRates[0];
+
+ for (int c = 1; c < refreshRates.length; c++) {
+ if (refreshRates[c] > largestRefreshRate) {
+ largestRefreshRate = refreshRates[c];
+ }
+ }
+
+ return MS_IN_A_SECOND / largestRefreshRate;
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ static class Api23Impl {
+ private Api23Impl() {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static float getFastestFrameTimeMs(Display display) {
+ Display.Mode[] displayModes = display.getSupportedModes();
+ float largestRefreshRate = displayModes[0].getRefreshRate();
+
+ for (int c = 1; c < displayModes.length; c++) {
+ float currentRefreshRate = displayModes[c].getRefreshRate();
+ if (currentRefreshRate > largestRefreshRate) {
+ largestRefreshRate = currentRefreshRate;
+ }
+ }
+
+ return MS_IN_A_SECOND / largestRefreshRate;
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ static class Api30Impl {
+ private Api30Impl() {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static Display getDisplayForContext(Context context) {
+ return context.getDisplay();
+ }
+ }
+}