Android - 拦截并传递所有触摸事件

17

我有一个覆盖整个屏幕大小的ViewGroup,希望用它来展示用户与应用程序交互时的效果,并仍然将onTouch事件传递给任何底层视图。

我对所有MotionEvents(不仅仅是DOWN事件)都感兴趣,所以onInterceptTouchEvent()不适用于此,因为如果返回true,我的覆盖层将消耗所有事件,如果返回false,它将只接收到DOWN事件(onTouch也是如此)。

我认为我可以重写Activity的dispatchTouchEvent(MotionEvent ev)方法,并在我的覆盖层中调用自定义触摸事件,但这会导致输入坐标不根据我的视图位置进行转换(例如,所有事件将似乎发生在实际触摸下方约20个像素,因为系统栏没有考虑在内)。

5个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
9
唯一正确的构建这样一个拦截器的方式是使用自定义的ViewGroup布局。 但仅仅实现ViewGroup.onInterceptTouchEvent()并不足以捕获每一个触摸事件,因为子视图可以调用ViewParent.requestDisallowInterceptTouchEvent():如果子视图这样做,你的视图将停止接收对interceptTouchEvent的调用(在这里有一个关于子视图何时会这样做的示例)。 这是我写的一个类(Java,很久以前...),当我需要类似的东西时使用它,它是一个自定义的FrameLayout,可以完全控制委托对象上的触摸事件。
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;

/**
 * A FrameLayout that allow setting a delegate for intercept touch event
 */
public class InterceptTouchFrameLayout extends FrameLayout {
    private boolean mDisallowIntercept;

    public interface OnInterceptTouchEventListener {
        /**
         * If disallowIntercept is true the touch event can't be stealed and the return value is ignored.
         * @see android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
         */
        boolean onInterceptTouchEvent(InterceptTouchFrameLayout view, MotionEvent ev, boolean disallowIntercept);

        /**
         * @see android.view.View#onTouchEvent(android.view.MotionEvent)
         */
        boolean onTouchEvent(InterceptTouchFrameLayout view, MotionEvent event);
    }

    private static final class DummyInterceptTouchEventListener implements OnInterceptTouchEventListener {
        @Override
        public boolean onInterceptTouchEvent(InterceptTouchFrameLayout view, MotionEvent ev, boolean disallowIntercept) {
            return false;
        }
        @Override
        public boolean onTouchEvent(InterceptTouchFrameLayout view, MotionEvent event) {
            return false;
        }
    }

    private static final OnInterceptTouchEventListener DUMMY_LISTENER = new DummyInterceptTouchEventListener();

    private OnInterceptTouchEventListener mInterceptTouchEventListener = DUMMY_LISTENER;

    public InterceptTouchFrameLayout(Context context) {
        super(context);
    }

    public InterceptTouchFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public InterceptTouchFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public InterceptTouchFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyle) {
        super(context, attrs, defStyleAttr, defStyle);
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
        mDisallowIntercept = disallowIntercept;
    }

    public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener interceptTouchEventListener) {
        mInterceptTouchEventListener = interceptTouchEventListener != null ? interceptTouchEventListener : DUMMY_LISTENER;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean stealTouchEvent = mInterceptTouchEventListener.onInterceptTouchEvent(this, ev, mDisallowIntercept);
        return stealTouchEvent && !mDisallowIntercept || super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean handled = mInterceptTouchEventListener.onTouchEvent(this, event);
        return handled || super.onTouchEvent(event);
    }
}
这个类提供了setInterceptTouchEventListener()方法,用于设置自定义的拦截器。 当需要禁止拦截触摸事件时,它会欺骗:将禁止传递给父视图,就像应该做的那样,但仍然拦截它们。然而,它不再让监听器拦截事件,因此如果监听器在拦截触摸事件时返回true,那么将被忽略(如果您愿意,可以选择抛出IllegalStateException)。 这样,您就可以在不干扰子视图行为的情况下透明地接收通过您的ViewGroup传递的每个触摸事件。

重写ViewGroup#dispatchTouchEvent有什么问题吗? - pskink
据我所知,dispatch方法不应该被覆盖。它包含安全相关的内容和其他一些东西。此外,我想知道如果子视图禁止拦截,我希望监听器能够知道。我没有深入研究代码,不知道覆盖dispatchTouchEvent是否安全。我只是认为这是拦截事件的最佳方式,可以最大程度地避免干扰Android触摸流程。监听器不应该破坏子视图的流程。 - Daniele Segato
@pskink 哦,我的做法允许侦听器在某些时候实际拦截触摸事件,除非已被禁止。使用dispatch,您没有这个能力。 - Daniele Segato
OP问道“我对所有MotionEvents感兴趣”,这就是他确切问题的答案。 - pskink
这对我有用。一个重要的注意事项,可能显而易见:InterceptTouchFrameLayout应该包含也会接收触摸事件的视图。我最初尝试将其绘制在它们上面,但没有完成工作。 - Bartholomew Furrow

6
我用了一个不完美的解决方案,但在这种情况下起作用。 我创建了另一个ViewGroup,其中包含一个可点击的子元素,其宽度和高度设置为fill_parent(在添加效果之前这是我的要求),并重写了onIntercept...()。在onIntercept...()中,我将事件传递给我的覆盖视图,传递到自定义的onDoubleTouch(MotionEvent)方法,在那里进行相关处理,而不会中断平台路由输入事件,并让底层viewgroup正常运行。 很容易!

这是否适用于系统覆盖,即在获取所有触摸数据并对其进行操作后,将触摸事件传递给操作系统/浏览器/其他应用程序?目前来看,我的覆盖层只能是全有或全无,而这让我感到烦恼!谢谢 :-D - While-E
3
由于安全原因,您不能轻易地“窥视”应用程序之外的所有触摸操作。尽管如此,使用SYSTEM_ALERT_WINDOW权限并创建透明覆盖层是可能的。我从未尝试过这种方法,但它在“忙碌的Android开发者指南2.3”的“Tapjacking”章节(第23章)中有描述。http://commonsware.com/AdvAndroid/ - Dori
2
你能发一些代码来展示你是如何做到这一点的吗?我不明白你是如何将普通路由分割并将所有motionEvents传递到你的自定义viewGroup和消费事件的视图中。 - Joakim Palmkvist
我知道这是一个旧帖子,但如果您能更详细地解释一下如何解决这个问题,那将会非常好。也许可以展示一些代码? - Eyad Alama
1
即使您将事件传递给另一个方法,仍然必须从onInterceptTouchEvent()返回true或false。如果您从ACTION_DOWN返回true,则层次结构中的其他视图将不再接收此手势的事件;如果您返回false,则此视图将不会接收此手势的后续MotionEvents。我可能忽略了某些东西,但在我看来,您可以采用覆盖Activity中的dispatchTouchEvent()的方法(监视而不是拦截),如问题所述,然后根据需要转换坐标。 - Mark McClelland

2

当触摸从根视图流向子视图并到达最底部的子视图后,它(触摸)会在遍历所有OnTouchListener时返回到根视图,如果由子视图消耗的动作返回true且其他动作返回false,则您想要拦截的动作可以编写在这些父项的OnTouchListner下。

例如,如果根视图A有两个子视图X和Y,并且您想要在A中拦截滑动触摸,并希望将其余触摸传播到子视图(X和Y),则通过返回false来覆盖X和Y的OnTouchListner以进行滑动,对于其余触摸返回true。并通过返回true来覆盖A的OnTouchListner以进行滑动,对于其余触摸返回false。


2

以下是我的做法,基于Dori的回答,我使用了onInterceptTouch . . .

float xPre = 0;

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if(Math.abs(xPre - event.getX()) < 40){
        return false;
    }
    if(event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_UP  || event.getAction() == MotionEvent.ACTION_MOVE){
        xPre = event.getX();
    }
    return true;
}

基本上,当任何重要的动作发生时,请记住它发生的地方,通过xPre;然后,如果该点足够接近下一个点,可以说是子视图的onClick/onTouch,返回false并让onTouch执行其默认操作。


在我看来,除非你返回false,否则这似乎不会产生将所有事件传递给层次结构中其他视图的期望效果。 - Mark McClelland

-1

我尝试了以上所有解决方案,但仍然存在问题:无法接收所有触摸事件,同时允许子视图消耗事件。在我的情况下,我想监听子视图将要消耗的事件。当一个重叠的子视图开始消耗事件后,我无法接收到后续事件。

最终,我找到了一个可行的解决方案。使用RxBinding库,您可以观察所有触摸事件,包括被重叠的子视图消耗的事件。

Kotlin代码片段

RxView.touches(view)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(

        object  : Observer<MotionEvent> {
            override fun onCompleted() {
            }

            override fun onNext(t: MotionEvent?) {
                Log.d("motion event onNext $t")
            }

            override fun onError(e: Throwable?) {
            }
        }
    )

更多详细信息,请参见这里这里


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