事件冲突原因与解决方案大解密

一直想写一下自己对事件分发是总结的,但是又不知道从何写起。那我们就先从一个一个案例,再到源码一步步深入总结。

案例

案例一:

  btn_click = (Button) findViewById(R.id.btn_click);
        btn_click.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("TAG","点击事件OnClick");
            }
        });
        btn_click.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                Log.i("TAG","点击事件OnTouch");

                return false;
            }
        });

我们先从一个按钮的点击与触摸事件开始,我们现在来猜一下,我们点击按钮打印一个,还是会打印两个呢?

如果setOnTouchListener返回true 的时候呢?

我们可以看到,当setOnTouchListener返回true 的时候,我们的点击事件里边的语句并没有执行,返回false的时候,才会执行。这是为什么呢?

我们可以看到在View的事件分发处理dispatchTouchEvent(MotionEvent event)里边,当setOnTouchListener返回true的时候,有个result的变量值为true,如果值为true,那么在接下来的流程里边onTouchEvent(event)方法将不会执行,我们会在onTouchEvent(event)中找到这么一个方法performClickInternal(),然后再不断的往下追寻,你就会发现这样一段代码

 final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

所以这个时候,我们可以明白当返回false的时候OnClickListener会执行打印语句,返回false的时候为什么不执行了吧。这个时候又让我想起了事件冲突的定义:事件只有一个,有多个方法想要处理。处理的对象不是我们想给的,这个时候我们就叫事件冲突。

案例二:

了解了事件冲突,那么我们接下来就开始了解事件分发了。

Activity:

public class EventDemoOneActivity extends AppCompatActivity {


    private EventViewB ev_b;
    private EventViewGroupA evg_a;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_event_demo_one);
        initView();
    }

    private void initView() {
        ev_b = (EventViewB) findViewById(R.id.ev_b);
        evg_a = (EventViewGroupA) findViewById(R.id.evg_a);
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        Log.i("TAG","...........................dispatchTouchEvent..............Activity");

        return super.dispatchTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        Log.i("TAG","...........................onTouchEvent..............Activity");

        return super.onTouchEvent(event);
    }
}

ViewGroup:

/**
 * 版本:1.0
 * 创建日期:2/23/21 10:21 AM
 * 描述:
 */
public class EventViewGroupA extends LinearLayout {
    public EventViewGroupA(Context context) {
        super(context);
    }

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


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        Log.i("TAG","...........................dispatchTouchEvent..............EventViewGroupA");

        return super.dispatchTouchEvent(ev);

    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        Log.i("TAG","...........................onInterceptTouchEvent..............EventViewGroupA");

        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        Log.i("TAG","...........................onTouchEvent..............EventViewGroupA");

        return true;
    }
}

View:

/**
 * 版本:1.0
 * 创建日期:2/23/21 10:35 AM
 * 描述:
 */
public class EventViewB extends View {


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

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


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        Log.i("TAG","...........................dispatchTouchEvent..............ViewB");

        return super.dispatchTouchEvent(event);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        Log.i("TAG","...........................onTouchEvent..............ViewB");

        return super.onTouchEvent(event);
    }
}

1.事件分发过程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三个方法协助完成
2.Android事件分发顺序:Activity(Window) -> ViewGroup -> View
3.在事件分发三大类(Activity、ViewGroup、View)中,Activity和View不会去拦截事件(也就是不能重写onInterceptTouchEvent()方法)
跑起来

我们再结合事件分发流程图来看

1.dispatchTouchEvent 和 onTouchEvent 一旦return true,事件就停止传递了(到达终点)(没有谁能再收到这个事件)。看图中只要return true事件就没再继续传下去了,对于return true我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁能再收到这个事件了。

2.dispatchTouchEvent 和 onTouchEvent方法在return false的时候事件都回传给父控件的onTouchEvent处理。

  • 对于dispatchTouchEvent 返回 false 的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。
  • 对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。

3.onInterceptTouchEvent 的作用

  • onInterceptTouchEvent方法中 return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的,是不会拦截的,事件会继续往子View的dispatchTouchEvent传递。

案例三

ViewPager:

/**
 * 版本:1.0
 * 创建日期:5/9/21 9:35 PM
 * 描述:
 */
public class BadViewPager extends ViewPager {


    private int mLastX,mLastY;


    public BadViewPager(@NonNull Context context) {
        super(context);
    }

    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }
}

ListView:

/**
 * 版本:1.0
 * 创建日期:5/9/21 9:37 PM
 * 描述:
 */
public class MyListView extends ListView {

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

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

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

    

}

Activity:

public class ListViewAndViewPagerActivity extends AppCompatActivity {


    private BadViewPager bad_view_pager;
    private MyPagerAdapter myPagerAdapter;
    private int[] iv = new int[]{R.mipmap.iv_0, R.mipmap.iv_1, R.mipmap.iv_2,
            R.mipmap.iv_3, R.mipmap.iv_4, R.mipmap.iv_5,
            R.mipmap.iv_6, R.mipmap.iv_7, R.mipmap.iv_8};


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view_and_view_pager);
        initView();
    }

    private void initView() {
        bad_view_pager = (BadViewPager) findViewById(R.id.bad_view_pager);


        List<Map<String, Integer>> strings = new ArrayList<>();

        Map<String, Integer> map;

        for (int i = 0; i < 20; i++) {
            map = new HashMap<>();
            map.put("key", iv[i % 9]);
            strings.add(map);
        }

        MyPagerAdapter adapter = new MyPagerAdapter(this, strings);
        bad_view_pager.setAdapter(adapter);

    }
}

当ViewPager返回true 无法上下滑动,当ViewPager返回false 无法左右滑动,如果把 onInterceptTouchEvent(MotionEvent ev) 都注释掉,那么上下左右滑动正常。这是因为谷歌工程师已经给我们在源码里边进行处理了。加入谷歌工程师并没有处理,那我们应该怎么做呢?

我们先来看事件分发流程图

ViewGroup的事件分发:

在ViewGroup的 boolean dispatchTouchEvent(MotionEvent ev) 中,当为Down事件的时候,会调用cancelAndClearTouchTargets(ev); resetTouchState();对状态进行清零。

清零后,会进入如下,进行判断时间是否拦截:

如果不拦截,进行分发,就会进入如下代码,进行分发或者处理:

在代码中,我可以看到,如果是down事件,才会分发。

里边有个方法是对子View进行排序

分发判断是进行倒序排序的,并判断能否接收事件:1、VISIBLE 2、child.getAnimation()!=null 3、触点在View的范围内。

进入分发处理事件,第三个参入传入的触点处的view

如果是ViewGroup就走ViewGroup的 dispatchTouchEvent,如果是View就走View的事件分发事件。(这个属于递归事件)

 

如果全部不处理,就跟拦截是一样的。

 

 

 

 

 

 

 

如果拦截,if (!canceled && !intercepted) 将进不去,直接走到下边(分发或者处理),事件到底处不处理,相当于是最后一个:

首先,第一次mFirstTouchTarget肯定为空,dispatchTransformedTouchEvent这个方法就是处理;

这个时候是最后一个,子View为空 。

所以,分发的最终又回到了View 的 dispatchTouchEvent(MotionEvent event),也就是回到了我们前边的案例一事件处理中

所以,当Viewpager拦截的时候,ListView是无法处理事件,所以没办法上下滑动。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

为true 就没办法事件分发。

我们再来看看move事件,move不会再走分发

 

总结:

ViewGroup需要先走分发流程,如果没人处理,就再走处理流程;View只能走处理流程

分发流程:1、先看是否拦截后自己处理  2、分发下去:排序、遍历分发、领取View的处理事件。3、没人处理,再看下自己是否处理事件;

move事件不会再走分发事件

那么我们应该怎么处理事件冲突?

1、内部拦截法,子View处理事件分发

View:

/**
 * 版本:1.0
 * 创建日期:5/9/21 9:37 PM
 * 描述:
 */
public class MyListView extends ListView {

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

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

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

    // 内部拦截法:子view处理事件冲突
    private int mLastX, mLastY;

    @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: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;

            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

}

Viewpager:(当它为Down事件的时候不拦截)

public class BadViewPager extends ViewPager {


    private int mLastX,mLastY;


    public BadViewPager(@NonNull Context context) {
        super(context);
    }

    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        if (ev.getAction()==MotionEvent.ACTION_DOWN){
            super.onInterceptTouchEvent(ev);
            return false;
        }

        return true;
    }
}

2、外部拦截法,是父容器处理冲突

Listview:


/**
 * 版本:1.0
 * 创建日期:5/9/21 9:37 PM
 * 描述:
 */
public class MyListView extends ListView {

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

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

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

    
}

ViewPager:

/**
 * 版本:1.0
 * 创建日期:5/9/21 9:35 PM
 * 描述:
 */
public class BadViewPager extends ViewPager {


    private int mLastX,mLastY;


    public BadViewPager(@NonNull Context context) {
        super(context);
    }

    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {


        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = (int) event.getX();
                mLastY = (int) event.getY();
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    return true;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        return super.onInterceptTouchEvent(event);

    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值