首页 > 代码库 > 事件分发机制【案例】【总结】

事件分发机制【案例】【总结】

技术分享

示例之Activity 

public class MainActivity  extends Activity implements OnTouchListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_second);
        findViewById(R.id.root).setTag("root");
        findViewById(R.id.ll_child).setTag("ll_child");
        findViewById(R.id.tv).setTag("tv");
        findViewById(R.id.tv2).setTag("tv2");
        findViewById(R.id.tv3).setTag("tv3");
        findViewById(R.id.tv_child).setTag("tv_child");
        findViewById(R.id.tv_child2).setTag("tv_child2");

        findViewById(R.id.root).setOnTouchListener(this);
        findViewById(R.id.ll_child).setOnTouchListener(this);
        findViewById(R.id.tv).setOnTouchListener(this);
        findViewById(R.id.tv2).setOnTouchListener(this);
        findViewById(R.id.tv_child).setOnTouchListener(this);
        findViewById(R.id.tv_child2).setOnTouchListener(this);
    }
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        //ACTION_DOWN = 0,按下动作;ACTION_UP = 1,离开动作;ACTION_MOVE = 2,移动动作
        Log.i("onTouch:""onTouch--" + v.getTag() + "--" + event.getAction());
        return false;
    }
}

示例之布局

<com.bqt.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#00f"
    android:orientation="vertical"
    android:padding="30dp" >
    <com.bqt.MyTextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="65dp"
        android:background="#0ff"
        android:gravity="center"
        android:text="LinearLayout的子View1" />
    <com.bqt.MyTextView
        android:id="@+id/tv2"
        android:layout_width="match_parent"
        android:layout_height="65dp"
        android:background="#ff0"
        android:gravity="center"
        android:text="LinearLayout的子View2" />
    <com.bqt.MyLinearLayout
        android:id="@+id/ll_child"
        android:layout_width="match_parent"
        android:layout_height="220dp"
        android:background="#000"
        android:orientation="vertical"
        android:padding="30dp" >
        <com.bqt.MyTextView
            android:id="@+id/tv_child"
            android:layout_width="match_parent"
            android:layout_height="65dp"
            android:background="#fff"
            android:gravity="center"
            android:text="子子View1" />
        <com.bqt.MyTextView
            android:id="@+id/tv_child2"
            android:layout_width="match_parent"
            android:layout_height="65dp"
            android:background="#f00"
            android:gravity="center"
            android:text="子子View2" />
    </com.bqt.MyLinearLayout>
    <com.bqt.MyTextView
        android:id="@+id/tv3"
        android:layout_width="match_parent"
        android:layout_height="65dp"
        android:background="#0ff"
        android:gravity="center"
        android:text="LinearLayout的子View3" />
</com.bqt.MyLinearLayout>

示例之MyLinearLayout

package com.bqt;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class MyLinearLayout extends LinearLayout {
    public MyLinearLayout(Context context) {
        super(context);
    }
    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("onTouch:""dispatchTouchEvent--" + getTag() + "--" + event.getAction());
        //if (getId() == R.id.ll_child) return true;//在【示例1】时添加
        return super.dispatchTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("onTouch:""onTouchEvent--" + getTag() + "--" + event.getAction());
        //if (getId() == R.id.ll_child) return true;//在【示例1】时增加此行代码没有任何意义。在【示例3】时添加。
        return super.onTouchEvent(event);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        Log.i("onTouch:""onInterceptTouchEvent--" + getTag() + "--" + event.getAction());
        //if (getId() == R.id.ll_child) return true;//在【示例5】时添加。
        return super.onInterceptTouchEvent(event);
    }
}

示例之MyTextView

public class MyTextView extends TextView {
    public MyTextView(Context context) {
        super(context);
    }
    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("onTouch:""dispatchTouchEvent--TextView--" + getTag() + "--" + event.getAction());
        //if (getId() == R.id.tv2) return true;//在【示例2】时添加
        return super.dispatchTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("onTouch:""onTouchEvent--" + getTag() + "--" + event.getAction());
        //if (getId() == R.id.tv2) return true;//在【示例2】时增加此行代码没有任何意义。在【示例4】时添加。在【示例5】时增加此行代码没有任何意义。
        return super.onTouchEvent(event);
    }
}

【示例1,VG的dispatchTE方法】

我们在【子LinearLayout】dispatchTouchEvent方法中返回true
或者在【子LinearLayout】dispatchTouchEvent和onTouchEvent方法中都返回true

当触摸根部的LinearLayout时
技术分享
当触摸根部的子View1时
技术分享
当触摸子LinearLayout或子子View1、子子View2
技术分享
可以看到:
1、触摸事件由Action_Down==0、Action_Move==2、Aciton_UP==1组成,其中一次完整的触摸事件中,Down只有一个、Up有一个或0个、Move有若干个(包括0个),一旦Aciton_UP发生,就表明此次触摸事件已经结束了

2、不管触摸哪里,不管是触摸事件中的哪个Action(DOWN、MOVE、UP),对于同一个View来说,都是按照下面的顺序执行的:
dispatchTouchEvent --> onTouch(如果有) --> onTouchEvent

3、如果触摸点区域没有一个View的dispatchTouchEvent方法的返回值为true(大前提是:也没有一个View的onTouchEvent方法onTouch方法的返回值为true),如当触摸根部的子View1时,则在分发完Down事件后(Down事件一定会分发),其余的Move、UP事件将不再分发,也即任何View都不会收到后续的Move、UP事件(后续事件不再分发),因为没有View需要处理。

4、同样是在上面的条件下,即:如果触摸点区域没有一个View的dispatchTouchEvent方法的返回值为true(大前提是:也没有一个View的onTouchEvent方法onTouch方法的返回值为true),如当触摸根部的子View1时,Down事件从root逐级下传到最底层的View1后,View1的dispatchTouchEvent又将Down事件传给了自己的onTouchEvent方法,然后是逐级逆向、上传给父View的onTouchEvent方法

5、一旦某个View(如子LinearLayout)的dispatchTouchEvent方法返回值为true(表明事件分发到此结束),则其子View(如子子View1、子子View2)就不可能会获取到任何Touch事件(包括Down事件);但是此View的所有父View(如root)可以获取到此View能获取到的任何事件(包括Move、UP事件) 。

6、如果一个ViewGroup的dispatchTouchEvent方法返回值为true,则此ViewGroup的onTouchEvent方法和onTouch方法将不会被调用。

【示例2,View的dispatchTE方法

我们在【子View2】dispatchTouchEvent方法中返回true
或者【子View2】dispatchTouchEvent和onTouchEvent方法中都返回true

当触摸其他View时的过程和上面的情况完全一样
当触摸根部的子View2时
技术分享
我们发现,示例1中的结论对于View来说也完全适用

【示例3,VG的onTouchEvent方法】

我们仅在【子LinearLayoutonTouchEvent方法中返回true

当触摸子LinearLayout
技术分享
可以发现,对于事件的分发过程,其和示例1、示例2完全一样。
唯一的区别是:
如果子LinearLayout的onTouchEvent方法返回true,那么子LinearLayout的onTouchEvent方法将不再逐级逆向、上传给父View的onTouchEvent方法,即其父View的onTouchEvent将无法调用
换句话说就是,如果子LinearLayout的onTouchEvent方法返回true,那么Touch事件将会在这里完全被消耗掉。

当触摸子子View1、子子View2
技术分享
可以发现:
1、对于【DOWN事件】,虽然子LinearLayout的onTouchEvent方法返回了true,但是子子View1dispatchTouchEvent方法仍【会】被调用,所以可以知道,任何View的onTouchEvent方法返回true都不影响DOWN事件的正常分发

2、同样,对于【DOWN事件】,子子View1dispatchTouchEvent被调用后会回调自己的onTouchEvent方法,然后子子View1onTouchEvent方法会回调其父ViewonTouchEvent方法。这些都和上面的分析是完全一样的。

3、下面是关键了,我们发现,对于【非DOWN事件】子子View1dispatchTouchEvent方法【不会】被调用(当然,子子View1onTouchEvent方法更不可能会被调用),这意味着分发过程在此之前已经结束了。

4、完整的流程为:一旦某个View的onTouchEvent返回true,当【DOWN】事件按照【正常的分发流程】逐级【下传】到【最底层的View】的【dispatchTouchEvent】后,【最底层的View】将通过其onTouchEvent】逐级【上传】,直到上传到【此View】的【onTouchEvent】方法之后,DOWN事件将会被消耗掉;然而,此后的【MOVE、UP】事件在按照【正常的分发流程】逐级【下传】到【此View】后将直接结束分发(而不会下传到最底层的View),并且在调用【此View】onTouchEvent方法后被消耗掉。

5、由此可见,MOVE、UP事件仅onTouchEvent返回true的那个View及其所有父View能接收到,这也是onTouchEvent方法最核心的作用,即用来告诉系统,Touch事件到底应该【传递】哪个View。注意,不要误解为:MOVE、UP事件仅onTouchEvent返回值为true的那个View才能接收到。而应该理解为:MOVE、UP事件仅onTouchEvent返回值为true的那个View的onTouchEvent方法才会被回调到

6、简单的说就是:
  • 事件【向下】传递过程中,如果中间任一View的dispatchTouchEvent方法返回true,则事件将结束分发(此时不会调用任何View的onTouchEvent方法)
  • 事件【向上】传递过程中,如果中间任View的onTouchEvent方法返回true,除了DOWN事件会正常分发外(会向下分发,dispatchTouchEvent方法会被正常回调,onTouchEvent方法会在回调到此View时结束回调),其他Touch事件在【分发】到此View之后将结束分发(不会再向下分发),且事件会在此View的onTouchEvent中被消费掉(不会再向上传递)。

【示例4,View的onTouchEvent方法】

我们仅在【子View2】onTouchEvent方法中返回true

当触摸其他View时的过程和上面的情况完全一样
当触摸根部的子View2时
技术分享
实在没啥好说的,该说的上面的都说完了。
也就是说,示例3所有的结论对于View来说也完全适用

【示例5,VG的onInterceptTE方法】

我们仅在【子LinearLayout】的onInterceptTouchEvent方法中返回true
或者【子LinearLayout】的onInterceptTouchEvent方法中返回true,并且在【子View2】onTouchEvent方法中返回true

当触摸其他View时的过程和上面的情况完全一样
当触摸子LinearLayout
技术分享
可以看到,在调用dispatchTouchEvent方法之后立刻调用了View的onInterceptTouchEvent方法,除此之外没任何区别。

当触摸子子View1和子子View2时:
技术分享
我们发现,事件分发过程中,子子View1、子子View2的任何回调方法都回调不到了,包括万能的DOWN事件
另外,虽然子View2的onTouchEvent方法返回true,但是也没有任何卵用,因为在dispatchTouchEvent下发DOWN事件的过程中,在DOWN事件分发到它之前就被拦截掉了,后续也更不会收到MOVE、UP事件。

这是onInterceptTouchEvent的一个重要作用:拦截包括Down事件在内的所有事件【向下】分发

【用onInterceptTE解决滑动冲突的思想】
设想一下在一个ViewPager中,每个Item都是个ImageView,我们需要对这些ImageView做Matrix操作,这不可避免要捕获掉Touch事件,但是我们又需要做到不影响ViewPager翻页效果,这又必须保证ViewPager能捕获到Move事件,我们该怎么做呢?
我们可以ViewPager的onInterceptTouchEvent接收到的Move事件做一个过滤,当适当条件的Move事件(如持续若干时间或移动若干距离)触发时,会拦截掉,返回子View一个Action_Cancel事件,这个时候子View就没有Up事件了,很多需要在Up中处理的事物要转到Cancel中处理。

【面试题:滑动冲突问题如何解决?】

答:
要解决滑动冲突,其实最主要的就是有一个核心思想:你到底想在一个事件序列中让哪个view 来响应你的滑动?

比如,从上到下滑,是哪个view来处理这个事件,从左到右呢?

业务需求想明白以后,剩下的其实就很好做了。核心的方法就是2个,外部拦截也就是父亲拦截,另外就是内部拦截,也就是子view拦截法。学会这2种,基本上所有的滑动冲突都是这2种的变种,而且核心代码思想都一样。

外部拦截法:
思路就是重写父容器的onInterceptTouchEvent即可,子元素一般不需要管。可以很容易理解,因为这和android自身的事件处理机制逻辑是一模一样的,例如:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN://down事件肯定不能拦截,拦截了后面的就收不到了
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (你的业务需求)  intercepted = true;//如果确定拦截了,就去自己的onTouchEvent里处理拦截之后的操作即可
            else  intercepted = false;
            break;
        case MotionEvent.ACTION_UP:
            //up事件我们一般都是返回false的,一般父容器都不会拦截他, 因为up是事件的最后一步,这里返回true也没啥意义。
            //唯一的意义就是,如果父元素把up拦截了,将导致子元素收不到up事件,
            //那子元素就肯定没有onClick事件触发了,这里的小细节 要想明白
            intercepted = false;
            break;
        default:
            break;
    }
    return intercepted;
}

内部拦截法:
内部拦截法稍微复杂一点,就是事件到来的时候,父容器不管,让子元素自己来决定是否处理。如果消耗了就最好,没消耗自然就转给父容器处理了。

子元素代码:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if (如果父容器需要这个点击事件) getParent().requestDisallowInterceptTouchEvent(false);
            //否则的话就交给自己本身view的onTouchEvent自动处理了
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(event);
}


父亲容器代码也要修改一下,其实就是保证父亲别拦截down事件:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN)  return false;
    return true;
}

事件分发机制【案例】【总结】