Get MotionEvent.getRawX/getRawY of other pointers

拟墨画扇 提交于 2019-12-05 01:38:39

Indeed, the API doesn't allow to do this, but you can compute it. Try that :

public boolean onTouch(final View v, final MotionEvent event) {

    int rawX, rawY;
    final int actionIndex = event.getAction() >> MotionEvent.ACTION_POINTER_ID_SHIFT;
    final int location[] = { 0, 0 };
    v.getLocationOnScreen(location);
    rawX = (int) event.getX(actionIndex) + location[0];
    rawY = (int) event.getY(actionIndex) + location[1];

}

The getLocationOnScreen answer works most of the time, but I was seeing it return incorrect values sometimes (when I was repositioning and re-parenting the view while the touch event was taking place), so I found an alternate approach that works more reliably.

If you look at the implementation of getRawX, it calls a private native function that accepts a pointerIndex, but the MotionEvent class only ever calls it with index 0:

public final float getRawX() {
    return nativeGetRawAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
}

Unfortunately, nativeGetRawAxisValue is private, but you can hack around that by using reflection to give yourself access to everything you need. Here's what the code looks like:

private Point getRawCoords(MotionEvent event, int pointerIndex) {
    try {
        Method getRawAxisValueMethod = MotionEvent.class.getDeclaredMethod(
                "nativeGetRawAxisValue", long.class, int.class, int.class, int.class);
        Field nativePtrField = MotionEvent.class.getDeclaredField("mNativePtr");
        Field historyCurrentField = MotionEvent.class.getDeclaredField("HISTORY_CURRENT");
        getRawAxisValueMethod.setAccessible(true);
        nativePtrField.setAccessible(true);
        historyCurrentField.setAccessible(true);

        float x = (float) getRawAxisValueMethod.invoke(null, nativePtrField.get(event),
                MotionEvent.AXIS_X, pointerIndex, historyCurrentField.get(null));
        float y = (float) getRawAxisValueMethod.invoke(null, nativePtrField.get(event),
                MotionEvent.AXIS_Y, pointerIndex, historyCurrentField.get(null));
        return new Point((int)x, (int)y);
    } catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException|
            NoSuchFieldException e) {
        throw new RuntimeException(e);
    }
}

Of course, the MotionEvent internals aren't documented, so this approach might crash on past or future versions of the SDK, but it seems to be working for me.

Edit: It looks like the type of mNativePtr and the nativePtr param changed from int to long in API level 20, so if you're targeting API level 19 or earlier, the above code will crash because getDeclaredMethod won't find anything. To fix this in my code, I just fetched the method by name instead of full type signature, which happens to work in this case. There isn't a way to directly look up methods with a given name, so I looped through the declared methods at static init time and saved the matching one to a static field. Here's the code:

private static final Method NATIVE_GET_RAW_AXIS_VALUE = getNativeGetRawAxisValue();
private static Method getNativeGetRawAxisValue() {
    for (Method method : MotionEvent.class.getDeclaredMethods()) {
        if (method.getName().equals("nativeGetRawAxisValue")) {
            method.setAccessible(true);
            return method;
        }
    }
    throw new RuntimeException("nativeGetRawAxisValue method not found.");
}

Then I used NATIVE_GET_RAW_AXIS_VALUE in place of the getRawAxisValueMethod in the above code.

It might be not enough to just shift local coordinates by a view's location if the view is rotated. In this case you need something like this:

void getRowPoint(MotionEvent ev, int index, PointF point){
    final int location[] = { 0, 0 };
    getLocationOnScreen(location);

    float x=ev.getX(index);
    float y=ev.getY(index);

    double angle=Math.toDegrees(Math.atan2(y, x));
    angle+=getRotation();

    final float length=PointF.length(x,y);

    x=(float)(length*Math.cos(Math.toRadians(angle)))+location[0];
    y=(float)(length*Math.sin(Math.toRadians(angle)))+location[1];

    point.set(x,y);
}

A solution worth trying for most use cases is to add this to the first line of the onTouchEvent it simply finds the difference between the raw and processed, and shifts the location of the MotionEvent event, by that amount. So that all the getX(int) values are now the raw values. Then you can actually use the getX() getY() stuff as the Raw values.

event.offsetLocation(event.getRawX()-event.getX(),event.getRawY()-event.getY());

While Ivan's point is valid, it's simply the case that applying a matrix directly to the view itself sucks so bad you likely shouldn't do it. It's weird and inconsistent between devices, cause the touch events to fall out of view and get declined, etc. If you are moving a view around like that you are better off simply overloading the onDraw() and applying that matrix to the canvas, then applying the inverse matrix to the MotionEvent so everything meshes up right. Then you can properly react to the events with proper and fine grain control. And, if you do that, my solution here wouldn't be subject to Ivan's objection.

There is no API to get pointer's specific RawX and RawY. But you can calculate similar values with regards to View's position to the parent and its rotation. In case if parent view occupies the entire touch area you are trying to handle, using Matrix will help you to solve your problem:

private float[] calcRawCoords(MotionEvent event, int pointerIndex) {
    Matrix screenMatrix = new Matrix();
    screenMatrix.postRotate(getRotation(), mPivotX, mPivotY);
    screenMatrix.postTranslate(getLeft(), getTop());
    float viewToScreenCoords[] = {event.getX(pointerIndex), event.getY(pointerIndex)};
    screenMatrix.mapPoints(viewToScreenCoords);
    return viewToScreenCoords;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!