View 的事件分发机制

简介: View 的事件分发机制

所谓事件分发,其实就是对 MotionEvent 事件的分发过程。即一个 MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View 。而这个传递的过程就是事件分发。


这个过程有三个很重要的方法来完成,如下


//用来进行事件分发,如果事件能过传递给当前View ,那么此方法一定会被调用。
//返回结果受当前View 的 onTouchEvent 和上下级 View 的dispatchTouchEvent 的影响,表示是否消耗此事件
public boolean dispatchTouchEvent(MotionEvent event)
//用来判断是否拦截某个事件,如果 当前View 拦截了某个事件,那么在同一个事件序列中,此方法不会被调用,
//返回结果表示是否拦截当前事件
public boolean onInterceptTouchEvent(MotionEvent event)
// onTouchEvent 在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件。
//如果不消耗,则在同一个事件序列中,当前View 无法再次接收到当前事件
public boolean onTouchEvent(MotionEvent event)


对于一个 ViewGroup 来说,点击事件产生后,首先会传给ViewGroup。这时他的 dispatchTouchEvent 就会被调用。如果这个ViewGroup 的 onInterceptTouchEvent 返回true ,则表示他要拦截此事件,接着事件就会交给ViewGroup 处理,即他的 onTouchEvent 就会被调用;如果ViewGroup 的 onInterceptTouchEvent 方法返回false,则表示不拦截此事件,这个时候事件就会继续向下传递,接着子View 的 dispatchTouchEvent 就会被调用。如此反复直到事件被处理。


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


重写此方法 返回 true 后,则表示 事件不在向下传递。接下来会调用 onTouchEvent 方法。onTouchEvent 返回true,则事件被消费。如果返回false ,则当前ViewGroup 无法接收到当前事件。


实验:


布局如下


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.admin.view_core.viewGroup.MyLinearLayout
      android:id="@+id/myLinearlayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#9999">
        <android.support.v7.widget.AppCompatButton
            android:id="@+id/btn1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="100dp" />
        <Button
            android:id="@+id/btn2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="100dp" />
    </com.admin.view_core.viewGroup.MyLinearLayout>
</RelativeLayout>


public class MyLinearLayout extends LinearLayout {
    private Paint paint;
    public MyLinearLayout(Context context) {
        super(context);
    }
    public MyLinearLayout(Context context,  AttributeSet attrs) {
        super(context, attrs);
    }
    public MyLinearLayout(Context context,AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    {
        paint = new Paint();
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Toast.makeText(getContext(), "onTouchEvent", Toast.LENGTH_SHORT).show();
        return true;
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.parseColor("#ED6F99"));
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(100,100,30,paint);
        canvas.drawCircle(300,300,50,paint);
        canvas.drawCircle(300,500,80,paint);
        canvas.drawCircle(700,300,20,paint);
    }
}


1,拦截事件,会发生什么?


@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;//拦截事件
    }


可以看到onTouchEvent 方法被调用了,说明拦截事件后 交给了 onTouchEvent 去处理了。如果没有拦截,则会向下传递


2, onTouchEvent 返回 true 和 false 有什么区别吗?


我们先给这个 ViewGroup 加一个 点击事件


findViewById(R.id.myLinearlayout).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "onClick", Toast.LENGTH_SHORT).show();
            }
        });


下面在看 返回 true 和 返回 false 的区别


返回true,(ViewGroup不拦截事件)。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        Toast.makeText(getContext(), "onTouchEvent", Toast.LENGTH_SHORT).show();
        return true;
    }


如果我们点击的 是 ViewGroup ,则onTouchEvent 消费掉事件,并且 onClick 不会执行,如果点击的是 ViewGroup 中的Button ,则 onTouchEvent 不会执行。


由此可得,当设置拦截器后 onTouchEvent 一定会执行,如果没有设置拦截器 且 点击的刚好是 ViewGroup ,则 onTouchEvent 也会执行,如果点击的是 ViewGroup 的子View。则onTouchEvent 不会执行。且 当 onTouchEvent 返回 true 时,ViewGroup 的onClick事件也没有执行。


返回false,(不拦截事件)。


同上,如果点击的是 子View 则继续向下传递,否则 ViewGroup 无法接收当前事件(没有消费此事件)。则该事件就会向上传递()。且 ViewGroup 的点onClick件 也没有执行。


返回 super。


点击事件执行,且onTouchEvent 消费了此事件,这个事件到这里结束。


当一个View 需要事件处理时,如果他设置了 OnTouchListener 。则 OnTouchListener 中的 onTouch 方法会被回调。这个事件如何处理则需要看 onTouch 的返回值。如果返回 false。则当前 View 的 onTouchEvent 会被调用。如果返回true,则不会被调用。由此可见 OnTouchListener 的优先级 比 OnTouchEvent 的优先级高。


注意:View 没有 拦截事件的方法!


实验:


布局如下


<com.admin.view_core.viewGroup.MyRelative xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
     <com.admin.view_core.view.CustomVolumeControlBar
         android:id="@+id/controlBar"
         android:layout_width="150dp"
         android:layout_height="150dp"
         app:secondColor="#000000"
         app:circleWidth="10dp"
         app:splitSize="20"
         app:dotCount="10"
         app:firstColor="#999999"
         app:bg="@drawable/ic_launcher"/>
</com.admin.view_core.viewGroup.MyRelative>


/**
 * @author Lv
 * Created at 2019/6/5
 */
public class CustomVolumeControlBar extends View
{
    ......
    public CustomVolumeControlBar(Context context) {
        this(context, null);
    }
    public CustomVolumeControlBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public CustomVolumeControlBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
       ......
        mPaint = new Paint();
        mRect = new Rect();
        rect = new Rect();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
  ......
        canvas.drawBitmap(mImage, null, mRect, mPaint);
    }
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {  
      Log.e("---------------", "onTouchEvent: " );
        return true;
    }
}


由于代码较长,所以我将 一些 代码省略了。


1,首先给 这个 View 设置 onClick 和 onTouch


CustomVolumeControlBar bar = findViewById(R.id.controlBar);
        bar.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "onClick: " );
            }
        });
        bar.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return true;
            }
        });


继续分析,如果 onTouch 返回 true 会如何。


结果就是 View 的 onClick 和 onTouchEvent 都没有执行


如果 onTouch 返回 false 呢(onTouchEvent 返回 true )?


结果 onClick 没有执行,但是 onTouchEvent 执行了。


onTouchEvent 返回 true,


结果:onClick 没有执行,onTouchEvent 消费了此事件。


onTouchEvent 返回 false


结果:onClick 没有执行,onTouchEvent 没有消费此事件,则此事件向上传递,父视图 的 onTouchEvent 会被调用。


onTouchEvent 返回 super


结果:onclick 执行,onTouchEvent 消费此事件。


由此可见,onTouch 的优先级 > onTouchEvent > onClick


当一个事件产生后,他的传递遵循如下规律:Activity -> Window -> View ,即事件总是先给 Activity,Activity 在传给 Window。最后 Window 在传给 顶级的 View 。


View 接收到事件后 ,就会按照事件分发机制去分发事件。如果一个 View 的 onTouchEvent 返回 false,那么他的父容器的 onTouchEvent 将会被调用,依次类推,如果所有的 onTouchEvent 都不处理这个事件,那么这个事件最终会被 Activity 所处理,即 Activity 的 onTouchEvent 会被调用。


下面看一张流程图:


0a2653c851af460fa595bd959398a8f1.png


在图片中,清晰的记录了从开始到结束 所调用的方法。这个图片来自这位大佛 https://blog.csdn.net/petterp/article/details/94463191


对于 dispatchTouchEvent (分发)方法:如果他返回 false ,不管当前是 ViewGroup 还是 View ,都会从 当前View 的上一级onTouchEvent 事件向上传递()。如果返回 true,则表示这个事件被消费,这个事件就直接结束。返回 super,事件继续向下分发,直到事件被消费为止。


关于事件的传递,这里给出一些结论,根据这些结论可以更好地理解整个传递机制,如下所示:


1,同一个序列 是指当 手指按下的那一刻起,到这个手指离开的那一刻结束,在这个过程中所产生的一系列事件,这个事件以 down 开始,中间 有不定的 move 事件,最后以 up 事件结束。


2,正常情况下,一个事件序列只能被一个 View 所拦截且消耗(3)。因为一旦一个元素拦截了 某个事件,那么同一个事件 序列内 的所有事件都会交给他处理,因此同一个时间序列中的事件不能分别由两个View 同时处理。但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件 通过 onTouchEvent 强行传递给其他 View处理。


3,某个 View 一旦决定拦截,那么这个序列只能由他 来处理(如果事件序列能高传递给他的话),并且它的 onInterceptTouchEvent 不会再被调用。这个也很好理解,就是说当某个View决定拦截一个事件后,那么系统会把同一个事件序列的其他方法都交给他处理,因此就不会调用这个 View 的 onInterceptTouchEvent 去询问他是否要拦截了。


4,某个View 一旦开始处理事件,如果他不消耗 ACTION_DOWN 事件 (onTouchEvent 反回了 false),那么同一事件序列的其他事件都不会交给他来处理,并且交给他的父元素去处理。即父元素的 onTouchEvent 会被调用,意思就是 事件一旦交给了 View去处理,那么他就必须消耗掉,否则 同一序列中剩下的事件 就不在交给他来处理了。


5,如果 View 不消耗 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且 View 可以持续收到后续的事件,最终这些消失的事件会传递给 Activity 处理。


6,ViewGroup 默认不会拦截任何事件。Android 源码中的 ViewGroup 的onInterceptTouchEvent 方法默认返回 false。


7,View没有 onInterceptTouchEvent 方法,一旦有事件传递给他,那么他的 onTouchEvent 就会被调用。


8,View 的 onTouchEvent 默认都会消耗事件(返回true),除非他是不可点击的(clickable 和 longClickable 同时 为 false),View 的longClickable 属性默认都为 false,clickable 属性要分情况,比如 Button 的 clickable 属性默认为 true,而 TextView 的 clickable 属性默认为 false。所以 如果给Button 的 clickable 设置为 false ,则这个Button 是不可点击的。


9.,View 额 enable 属性不影响 onTouchEvent 的返回值,哪怕一个 View 是 disable 状态的,只要他的 clickable 或者 longClickable 有一个 为 true,那么太尬的 onTouchEvent 就返回 true。


10,onClick 会发生的前提是 当前 View 是可点击的。并且他收到了 down 和 up 的事件。


11,事件 传递的过程是 由外向内的,即事件总是先传递给父元素,然后由父元素分发给 子View,通过 requestDisallowInterceptTouchEvent 方法可以 在子元素中敢于父元素的分发过程,但是 ACTION_DOWN 事件除外。


相关文章
|
8月前
|
XML Android开发 数据格式
Android 了解View的事件分发详解
Android 了解View的事件分发详解
53 0
|
8月前
|
Android开发
Android 中ViewPager嵌套RecyclerView出现滑动冲突的解决方案
Android 中ViewPager嵌套RecyclerView出现滑动冲突的解决方案
707 0
|
Android开发 容器
深入了解View的滑动冲突
在《与滑动冲突的首次邂逅》一文中,笔者举了一个开发过程中出现的一个简单的滑动冲突问题,带大家直观的了解何为滑动冲突,并且使用了内部拦截法(内部解决法)来解决了这个滑动冲突。
深入了解View的滑动冲突
SwipeRefreshLayout 嵌套 RecyclerView滑动冲突
SwipeRefreshLayout 嵌套 RecyclerView滑动冲突
263 0
|
Android开发
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )(二)
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )(二)
118 0
|
Android开发
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )(一)
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )(一)
136 0
|
Java Android开发
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 七 )(五)
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 七 )(五)
97 0
|
Android开发
android之View Touch事件的传递流程
android应用界面的布局如下图所示,一般我们在展示一个界面时会使用一个Activity表示;而Activity时通过Window展示的,android的Window实现类叫PhoneWindow;PhoneWindow类有个mDecor的DectorView全局变量,用来对界面的View元素进行修饰;DectorView用来修饰ActionBar、ContentView(Activity.setContentView);ContentView里面包含了用户自定义的一些子Layout。
1179 0
|
Java Android开发

热门文章

最新文章