Android:检测用户是否触摸并拖出按钮区域?

58
在Android中,我们如何检测用户是否点击按钮并将其拖出按钮区域?
10个回答

98

检查 MotionEvent.MOVE_OUTSIDE: 检查 MotionEvent.MOVE:

private Rect rect;    // Variable rect to hold the bounds of the view

public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
        // Construct a rect of the view's bounds
        rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());

    }
    if(event.getAction() == MotionEvent.ACTION_MOVE){
        if(!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())){
            // User moved outside bounds
        }
    }
    return false;
}

注意: 如果你想要面向 Android 4.0 版本,一整个新的世界将为你打开: http://developer.android.com/reference/android/view/MotionEvent.html#ACTION_HOVER_ENTER


1
抱歉,它不起作用。我只能捕获ACTION_UP、ACTION_DOWN和ACTION_MOVE,但无法捕获ACTION_OUTSIDE。 - anticafe
它从 API 级别 3 开始提供:http://developer.android.com/reference/android/view/MotionEvent.html#ACTION_OUTSIDE - Entreco
4
根据这个答案(https://dev59.com/-m025IYBdhLWcg3wLizo#8059266),我认为我们只能使用ACTION_OUTSIDE来检测触摸是否在Activity外部发生,而不能检测视图。 - anticafe
1
请参考 FrostRocket 的答案,更新 rect.contains 测试以使其适用于未定位在原点的单个视图。 - Dan J
我认为如果你想获取 ACTION_OUTSIDE,你需要使用 getActionMasked()。或者你可以比较掩码: if ((event.getAction() & MotionEvent.ACTION_OUTSIDE) == MotionEvent.ACTION_OUTSIDE)。这是因为 ACTION_OUTSIDE 不会在没有其他操作的情况下触发,所以它将被屏蔽。 - Chad Bingham
显示剩余2条评论

22

Entreco发布的答案在我的情况下需要进行一些微调。我不得不替换:

if(!rect.contains((int)event.getX(), (int)event.getY()))
if(!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY()))

因为event.getX()event.getY()仅适用于ImageView本身,而非整个屏幕。


+1 谢谢,这是更通用的方法,也适用于自定义视图实现。 - Knickedi

13
我遇到了与OP相同的问题,即我想知道何时(1)特定的View被按下,以及何时(2a)在View上释放按下的触摸或者(2b)当按下的触摸移动到View范围之外时。我将这个主题中的各种答案汇集起来,创建了一个名为SimpleTouchListenerView.OnTouchListener简单扩展,这样其他人就不必对MotionEvent对象进行操作。该类的源代码可以在此处或本回答底部找到。
要使用此类,只需将其设置为View.setOnTouchListener(View.OnTouchListener)方法的参数,如下所示:
myView.setOnTouchListener(new SimpleTouchListener() {

    @Override
    public void onDownTouchAction() {
        // do something when the View is touched down
    }

    @Override
    public void onUpTouchAction() {
        // do something when the down touch is released on the View
    }

    @Override
    public void onCancelTouchAction() {
        // do something when the down touch is canceled
        // (e.g. because the down touch moved outside the bounds of the View
    }
});

这是该类的源代码,您可以将其添加到您的项目中:

public abstract class SimpleTouchListener implements View.OnTouchListener {

    /**
     * Flag determining whether the down touch has stayed with the bounds of the view.
     */
    private boolean touchStayedWithinViewBounds;

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchStayedWithinViewBounds = true;
                onDownTouchAction();
                return true;

            case MotionEvent.ACTION_UP:
                if (touchStayedWithinViewBounds) {
                    onUpTouchAction();
                }
                return true;

            case MotionEvent.ACTION_MOVE:
                if (touchStayedWithinViewBounds
                        && !isMotionEventInsideView(view, event)) {
                    onCancelTouchAction();
                    touchStayedWithinViewBounds = false;
                }
                return true;

            case MotionEvent.ACTION_CANCEL:
                onCancelTouchAction();
                return true;

            default:
                return false;
        }
    }

    /**
     * Method which is called when the {@link View} is touched down.
     */
    public abstract void onDownTouchAction();

    /**
     * Method which is called when the down touch is released on the {@link View}.
     */
    public abstract void onUpTouchAction();

    /**
     * Method which is called when the down touch is canceled,
     * e.g. because the down touch moved outside the bounds of the {@link View}.
     */
    public abstract void onCancelTouchAction();

    /**
     * Determines whether the provided {@link MotionEvent} represents a touch event
     * that occurred within the bounds of the provided {@link View}.
     *
     * @param view  the {@link View} to which the {@link MotionEvent} has been dispatched.
     * @param event the {@link MotionEvent} of interest.
     * @return true iff the provided {@link MotionEvent} represents a touch event
     * that occurred within the bounds of the provided {@link View}.
     */
    private boolean isMotionEventInsideView(View view, MotionEvent event) {
        Rect viewRect = new Rect(
                view.getLeft(),
                view.getTop(),
                view.getRight(),
                view.getBottom()
        );

        return viewRect.contains(
                view.getLeft() + (int) event.getX(),
                view.getTop() + (int) event.getY()
        );
    }
}

2
这是一个很好的答案,特别适用于那些在处理 ACTION_CANCEL 未按预期触发问题时工作的人。 - New Guy
2
是的,这是一个很棒的答案! - Boris Karloff

8

我在我的OnTouch方法中加入了一些日志,发现触发了。这对我来说是足够好的...


2
如果您的View包含在父ViewGroup(如ScrollView)中,该父容器有兴趣从其子元素中获取移动触摸事件,则会收到MotionEvent.ACTION_CANCEL回调。但是,否则不能保证您的View会收到MotionEvent.ACTION_CANCEL回调。 - Adil Hussain
唯一有效的解决方案,谢谢你救了我。 - Mohammad Elsayed

3

除非视图位于 ScrollView 中,否则前两个答案都是可行的:当由于手指移动而发生滚动时,仍会将其注册为触摸事件,但不会作为 MotionEvent.ACTION_MOVE 事件。 因此,要改进答案(仅当您的视图位于滚动元素内时才需要):

private Rect rect;    // Variable rect to hold the bounds of the view

public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
        // Construct a rect of the view's bounds
        rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());

    } else if(rect != null && !rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())){
        // User moved outside bounds
    }
    return false;
}

我在安卓4.3和安卓4.4上进行了测试。

我没有注意到Moritz的答案和前两个答案之间有任何区别,但这也适用于他的答案:

private Rect rect;    // Variable rect to hold the bounds of the view

public boolean onTouch(View v, MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN){
        // Construct a rect of the view's bounds
        rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());

    } else if (rect != null){
        v.getHitRect(rect);
        if(rect.contains(
                Math.round(v.getX() + event.getX()),
                Math.round(v.getY() + event.getY()))) {
            // inside
        } else {
            // outside
        }
    }
    return false;
}

我刚刚在RecyclerView的一个项目项中的按钮上实现了这段代码,它完美地工作了。我只是想说,如果你在getHitRect之前添加它,就可以在ACTION_DOWN上摆脱new Rect了。 Rect rect = new Rect(); getHitRect(rect); - Jose M Lechon

3

可复用的 Kotlin 解决方案

我从两个自定义扩展函数开始:

val MotionEvent.up get() = action == MotionEvent.ACTION_UP

fun MotionEvent.isIn(view: View): Boolean {
    val rect = Rect(view.left, view.top, view.right, view.bottom)
    return rect.contains((view.left + x).toInt(), (view.top + y).toInt())
}

然后监听视图上的触摸。只有在 ACTION_DOWN 最初在视图上时才会触发此操作。当您松开手指时,它将检查手指是否仍在视图上。

myView.setOnTouchListener { view, motionEvent ->
    if (motionEvent.up && !motionEvent.isIn(view)) {
        // Talk your action here
    }
    false
}

2
view.setClickable(true);
view.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (!v.isPressed()) {
            Log.e("onTouch", "Moved outside view!");
        }
        return false;
    }
});

view.isPressed使用view.pointInView并包含一些触摸误差。如果您不想要误差,请直接复制内部的view.pointInView逻辑(该逻辑是公开的,但被隐藏起来,因此它不是官方API的一部分,可能随时消失)。

view.setClickable(true);
view.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            v.setTag(true);
        } else {
            boolean pointInView = event.getX() >= 0 && event.getY() >= 0
                    && event.getX() < (getRight() - getLeft())
                    && event.getY() < (getBottom() - getTop());
            boolean eventInView = ((boolean) v.getTag()) && pointInView;
            Log.e("onTouch", String.format("Dragging currently in view? %b", pointInView));
            Log.e("onTouch", String.format("Dragging always in view? %b", eventInView));
            v.setTag(eventInView);
        }
        return false;
    }
});

1
这里是一个可用的 View.OnTouchListener,您可以使用它来查看用户在手指离开视图时是否将发送 MotionEvent.ACTION_UP
private OnTouchListener mOnTouchListener = new View.OnTouchListener() {

    private Rect rect;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (v == null) return true;
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
            return true;
        case MotionEvent.ACTION_UP:
            if (rect != null
                    && !rect.contains(v.getLeft() + (int) event.getX(),
                        v.getTop() + (int) event.getY())) {
                // The motion event was outside of the view, handle this as a non-click event

                return true;
            }
            // The view was clicked.
            // TODO: do stuff
            return true;
        default:
            return true;
        }
    }
};

1
虽然@FrostRocket的答案是正确的,但您应该使用view.getX()和Y来考虑平移变化:
 view.getHitRect(viewRect);
 if(viewRect.contains(
         Math.round(view.getX() + event.getX()),
         Math.round(view.getY() + event.getY()))) {
   // inside
 } else {
   // outside
 }

0
如果拖动到视图之外,则调用“ACTION_CANCEL”事件。因此需要禁止父视图拦截触摸事件:
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            (parent as? ViewGroup?)?.requestDisallowInterceptTouchEvent(true)
        }
        MotionEvent.ACTION_UP -> {
            (parent as? ViewGroup?)?.requestDisallowInterceptTouchEvent(false)
        }
        else -> {
        }
    }
    return true
}

然后你可以检查触摸点是否在你的视图之外!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接