拒绝外围触摸输入

时间:2019-02-08 09:17:44

标签: android android-layout

给出一个嵌套的视图结构,例如:

Nested app layout

如何禁用灰色外围的输入,以使即使用户在手指上握住手指但触摸黄色视图,触摸事件仍会在dispatchTouchEvent()中注册,以便该视图不受阻碍?

编辑: 为了进一步解释,我需要在灰色区域使用某种手掌剔除系统。在黄色区域中,用户可以用手指绘画。所有这些都可以正常工作,但是在某些带有无边框显示器的手机上,您可能会不小心触摸灰色区域,该区域会被注册为输入,从而破坏了图形。

仅当用户一次在多个位置触摸屏幕时才会发生这种情况。在这一点上,可能有人会责怪用户购买这种mm头,但我已经亲自尝试过,不小心碰到边缘并防止事件正常进行很容易。

发布此问题后,我想出了一个使用多点触摸事件的有点棘手的解决方案。它比以前更好,但是因为它实际上不是多点触摸事件,所以有时会过时并完全停止注册输入。此外,最好能最终在黄色框中捕获实际的多点触摸事件(例如做出放大到绘图的手势)。

到目前为止,我的解决方案的基本前提是:

  1. 设置视图。在这个测试用例中,我只有一个视图,该视图绘制了onDraw()中的所有区域。
  2. 捕获MotionEvent中的dispatchTouchEvent()
  3. 用黄色区域内的x,y拾取事件的一部分:

然后:

// ... event:MotionEvent, pointerCoordsOut:MotionEvent.PointerCoords
for (pidx in 0 until event.pointerCount) {
    event.getPointerCoords(pidx, pointerCoordsOut)
    if (inYellowArea(pointerCoordsOut.x, pointerCoordsOut.y)) {
         //pointerCoordsOut now has (x,y) that I need
    }
}

最后,调整代码以接受ACTION_*ACTION_POINTER_*事件,并使它们做一些合理的事情。在演示案例中这很容易,但是我认为这是解决方案最终失败的地方。

因此,我仍然希望有一个正确的解决方案,以解决边界上的手掌不适感(如果该事件根本不存在),而在我看来,这是一个复杂手势的一部分,我正在尝试解密。

编辑

仍然开放征求意见。

2 个答案:

答案 0 :(得分:3)

已更新
布局:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/darker_gray"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/orange"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="64dp"
        android:layout_marginEnd="64dp"
        android:layout_marginStart="64dp"
        android:layout_marginTop="64dp"
        android:background="@android:color/holo_orange_dark"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

代码:

class MainActivity : AppCompatActivity() {

    private val touchableRect = Rect()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        root.post {
            orange.getGlobalVisibleRect(touchableRect)
        }

        root.setOnTouchListener { v, event ->
            Log.d("logi", "root touched : ${event.actionMasked}")
            false
        }

        orange.setOnTouchListener { v, event ->
            Log.d("logi", "orange touched : ${event.actionMasked}")
            true
        }
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        val isConsumed = ev?.let {
            if (touchableRect.contains(it.x.toInt(), it.y.toInt())) {
                orange.dispatchTouchEvent(it)
            } else {
                true
            }
        }

        return isConsumed ?: true
    }
}

答案 1 :(得分:0)

潜在的问题有两个:首先,除非有适当的编码来重定向手势,否则在手势持续时间内,绘图区域外部的触摸将永远无法到达绘图区域。其次,屏幕上可能有多个指针,因此必须保持笔直。

在以下解决方案中,从主活动的绘图区域调用dispatchTouchEvent()。这样可以确保绘图区域可以看到所有事件,即使是在其边界之外发出的事件。 MainActivity对绘图区域进行了总括呼叫,尽管在某些情况下可以更智能地调度呼叫。

绘图区域是一个简单的自定义视图,该视图对事件进行排序并在与其他事件密切相关的事件上起作用,而忽略其他事件。请参阅代码中的注释。自定义视图仅允许用户用手指绘制。

如果您想尝试一下,主要活动,布局和自定义视图都可以作为一个简单的应用程序一起工作。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private View mDrawingArea;
    private final int[] mDrawingAreaLocation = new int[2];

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View layout = findViewById(R.id.layout);
        mDrawingArea = findViewById(R.id.drawing_area);

        layout.post(new Runnable() {
            @Override
            public void run() {
                mDrawingArea.getLocationOnScreen(mDrawingAreaLocation);
            }
        });
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        // Everything goes to the drawing area. We could do some bounds checking and send
        // everything within the bounds to the drawing area and everything else to
        // super.dispatchTouchEvent().

        // Adjust location to be view-relative.
        event.offsetLocation(-mDrawingAreaLocation[0], -mDrawingAreaLocation[1]);
        return mDrawingArea.dispatchTouchEvent(event);
    }
}

MyView.java

public class MyView extends View {
    private final Rect mViewBounds = new Rect();
    private static final int NO_POINTER_CAPTURED = -1;
    private int mCapturedPointer = NO_POINTER_CAPTURED;
    private final Path mPath = new Path();
    private final Paint mPaint = new Paint();

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint.setStrokeWidth(10);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mViewBounds.left = 0;
        mViewBounds.top = 0;
        mViewBounds.right = w;
        mViewBounds.bottom = h;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Coordinates will be relative to the view.

        switch (event.getActionMasked()) {

            case MotionEvent.ACTION_DOWN:
                // This is the start of the gesture and the first pointer down.
                // Check to see if it is in our drawing area and capture it if it is.
                if (mViewBounds.contains((int) event.getX(), (int) event.getY())) {
                    mCapturedPointer = event.getPointerId(event.getActionIndex());
                    mPath.reset();
                    mPath.moveTo(event.getX(), event.getY());
                }
                break;

            case MotionEvent.ACTION_POINTER_DOWN:
                // This is the second, third, etc. pointer down. We are just going to
                // consider capturing it if we don't have another pointer already captured.
                if (mCapturedPointer == NO_POINTER_CAPTURED) {
                    int x = (int) event.getX(event.getActionIndex());
                    int y = (int) event.getY(event.getActionIndex());
                    // The pointer must be within the drawing area.
                    if (mViewBounds.contains(x, y)) {
                        mPath.reset();
                        mPath.moveTo(x, y);
                        mCapturedPointer = event.getPointerId(event.getActionIndex());
                    }
                }
                break;

            case MotionEvent.ACTION_MOVE:
                // See if our captured pointer is moving. If it is, do some prep for drawing.
                if (mCapturedPointer != NO_POINTER_CAPTURED) {
                    int pointerCount = event.getPointerCount();
                    for (int ptrIndex = 0; ptrIndex < pointerCount; ptrIndex++) {
                        if (event.getPointerId(ptrIndex) == mCapturedPointer) {
                            mPath.lineTo(event.getX(ptrIndex), event.getY(ptrIndex));
                            invalidate();
                            break;
                        }
                    }
                }
                break;

            case MotionEvent.ACTION_POINTER_UP:
                // Release the captured pointer when it leaves the surface.
                if (event.getPointerId(event.getActionIndex()) == mCapturedPointer) {
                    mCapturedPointer = NO_POINTER_CAPTURED;
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mCapturedPointer = NO_POINTER_CAPTURED;

            default:
                break;
        }

        // We want to look at everything.
        return true;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
    }
}

activity_main.xml

<FrameLayout 
    android:id="@+id/parentGroup"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_red_light"
    android:padding="32dp"
    tools:context=".MainActivity">

    <com.example.android.rejectperipheraltouches.MyView
        android:id="@+id/drawing_area"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp"
        android:background="#FDD835"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


</FrameLayout>

使用触摸监听器

以上代码重定向了活动中dispatchTouchEvent()中的事件,但对于更复杂的布局可能需要其他逻辑。一种替代方法是直接从触摸侦听器为父视图调用工程图视图的onTouchEvent()。因此,如果直接触摸绘图视图,则所有事件都会正常进行。但是,如果初始触摸是包围视图,则这些触摸将被重定向到工程图视图。这可能比dispatchTouchEvent()解决方案更干净。

MainActivity.java(可选)

public class MainActivity extends AppCompatActivity {
    private View mDrawingArea;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDrawingArea = findViewById(R.id.drawing_area);
        findViewById(R.id.parentGroup).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                event.offsetLocation(-mDrawingArea.getLeft(), -mDrawingArea.getTop());
                return mDrawingArea.onTouchEvent(event);
            }
        });
    }
}