blob: f6841f7d296a2d0e3cb8c6fdb04488eb7c75a1da [file] [log] [blame]
/*
* Copyright 2022 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.kalman;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.util.Log;
import android.util.SparseArray;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import java.util.Locale;
/**
*/
@RestrictTo(LIBRARY)
public class MultiPointerPredictor implements KalmanPredictor {
private static final String TAG = "MultiPointerPredictor";
private static final boolean DEBUG_PREDICTION = Log.isLoggable(TAG, Log.DEBUG);
private final SparseArray<SinglePointerPredictor> mPredictorMap = new SparseArray<>();
private int mReportRateMs = 0;
public MultiPointerPredictor() {}
@Override
public void setReportRate(int reportRateMs) {
if (reportRateMs <= 0) {
throw new IllegalArgumentException(
"reportRateMs should always be a strictly" + "positive number");
}
mReportRateMs = reportRateMs;
for (int i = 0; i < mPredictorMap.size(); ++i) {
mPredictorMap.valueAt(i).setReportRate(mReportRateMs);
}
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
int action = event.getActionMasked();
int actionIndex = event.getActionIndex();
int pointerId = event.getPointerId(actionIndex);
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
SinglePointerPredictor predictor = new SinglePointerPredictor(
pointerId,
event.getToolType(actionIndex)
);
if (mReportRateMs > 0) {
predictor.setReportRate(mReportRateMs);
}
predictor.onTouchEvent(event);
mPredictorMap.put(pointerId, predictor);
} else if (action == MotionEvent.ACTION_UP) {
SinglePointerPredictor predictor = mPredictorMap.get(pointerId);
if (predictor != null) {
mPredictorMap.remove(pointerId);
predictor.onTouchEvent(event);
}
mPredictorMap.clear();
} else if (action == MotionEvent.ACTION_POINTER_UP) {
SinglePointerPredictor predictor = mPredictorMap.get(pointerId);
if (predictor != null) {
mPredictorMap.remove(pointerId);
predictor.onTouchEvent(event);
}
} else if (action == MotionEvent.ACTION_CANCEL) {
mPredictorMap.clear();
} else if (action == MotionEvent.ACTION_MOVE) {
for (int i = 0; i < mPredictorMap.size(); ++i) {
mPredictorMap.valueAt(i).onTouchEvent(event);
}
} else {
// ignore other events
return false;
}
return true;
}
/** Support eventTime */
@Override
public @Nullable MotionEvent predict(int predictionTargetMs) {
final int pointerCount = mPredictorMap.size();
// Shortcut for likely case where only zero or one pointer is on the screen
// this logic exists only to make sure logic when one pointer is on screen then
// there is no performance degradation of using MultiPointerPredictor vs
// SinglePointerPredictor
// TODO: verify performance is not degraded by removing this shortcut logic.
if (pointerCount == 0) {
if (DEBUG_PREDICTION) {
Log.d(TAG, "predict() -> null: no pointer on screen");
}
return null;
}
if (pointerCount == 1) {
SinglePointerPredictor predictor = mPredictorMap.valueAt(0);
MotionEvent predictedEv = predictor.predict(predictionTargetMs);
if (DEBUG_PREDICTION) {
Log.d(TAG, "predict() -> MotionEvent: " + predictedEv);
}
return predictedEv;
}
// Predict MotionEvent for each pointer
int[] pointerIds = new int[pointerCount];
MotionEvent[] singlePointerEvents = new MotionEvent[pointerCount];
for (int i = 0; i < pointerCount; ++i) {
pointerIds[i] = mPredictorMap.keyAt(i);
SinglePointerPredictor predictor = mPredictorMap.valueAt(i);
singlePointerEvents[i] = predictor.predict(predictionTargetMs);
}
// Compute minimal history size for every predicted single pointer MotionEvent
boolean foundNullPrediction = false;
int minHistorySize = Integer.MAX_VALUE;
for (MotionEvent ev : singlePointerEvents) {
if (ev == null) {
foundNullPrediction = true;
break;
}
if (ev.getHistorySize() < minHistorySize) {
minHistorySize = ev.getHistorySize();
}
}
if (foundNullPrediction) {
for (MotionEvent ev : singlePointerEvents) {
if (ev != null) {
ev.recycle();
}
}
return null;
}
// Take into account the current event of each predicted MotionEvent
minHistorySize += 1;
// Merge single pointer MotionEvent into a single MotionEvent
MotionEvent.PointerCoords[][] pointerCoords =
new MotionEvent.PointerCoords[minHistorySize][pointerCount];
for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
int historyIndex = 0;
for (BatchedMotionEvent ev :
BatchedMotionEvent.iterate(singlePointerEvents[pointerIndex])) {
pointerCoords[historyIndex][pointerIndex] = ev.coords[0];
if (minHistorySize <= ++historyIndex) {
break;
}
}
}
// Recycle single pointer predicted MotionEvent
for (MotionEvent ev : singlePointerEvents) {
ev.recycle();
}
// Generate predicted multi-pointer MotionEvent
final MotionEvent.PointerProperties[] pointerProperties =
new MotionEvent.PointerProperties[pointerCount];
for (int i = 0; i < pointerCount; i++) {
pointerProperties[i] = new MotionEvent.PointerProperties();
pointerProperties[i].id = pointerIds[i];
}
MotionEvent multiPointerEvent =
MotionEvent.obtain(
0 /* down time */,
0 /* event time */,
MotionEvent.ACTION_MOVE /* action */,
pointerCount /* pointer count */,
pointerProperties /* pointer properties */,
pointerCoords[0] /* pointer coordinates */,
0 /* meta state */,
0 /* button state */,
1.0f /* x */,
1.0f /* y */,
0 /* device ID */,
0 /* edge flags */,
0 /* source */,
0 /* flags */);
for (int historyIndex = 1; historyIndex < minHistorySize; historyIndex++) {
multiPointerEvent.addBatch(0, pointerCoords[historyIndex], 0);
}
if (DEBUG_PREDICTION) {
final StringBuilder builder =
new StringBuilder(
String.format(
Locale.ROOT,
"predict() -> MotionEvent: (pointerCount=%d, historySize=%d);",
multiPointerEvent.getPointerCount(),
multiPointerEvent.getHistorySize()));
for (BatchedMotionEvent motionEvent : BatchedMotionEvent.iterate(multiPointerEvent)) {
builder.append(" ");
for (MotionEvent.PointerCoords coord : motionEvent.coords) {
builder.append(String.format(Locale.ROOT, "(%f, %f)", coord.x, coord.y));
}
builder.append("\n");
}
Log.d(TAG, builder.toString());
}
return multiPointerEvent;
}
}