Android自定义View

View事件分发(四) - View事件分发(源码分析)

2019-01-27  本文已影响12人  世道无情

1. 概述


前两篇文章记录了View事件分发的一些理论基础,这篇文章主要 从 View的 dispatchTouchEvent 源码角度 分析下 View事件分发流程;

下边通过一个示例代码来分析

2. 示例如下


创建 activity_main 布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        android:layout_centerInParent="true"
        />

</RelativeLayout>

给Button设置 setOnClickListener、setOnTouchListener事件

public class TextViewActivity extends AppCompatActivity {

    private Button btn1;

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

        btn1 = (Button) findViewById(R.id.btn1);

        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("TAG" , "onClick") ;
            }
        });

        btn1.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e("TAG" , "onTouch , action: "+event.getAction()) ;
                return false;
            }
        });
    }

}

0代表down、1代表up,2代表move,一般可能有多个move:

onTouch返回true,log如下:

onTouch , action: 0
onTouch , action: 2
onTouch , action: 2
...
onTouch , action: 1

onTouch返回false,log如下:

onTouch , action: 0
onTouch , action: 2
onTouch , action: 2
...
onTouch , action: 1
onClick

现象是:
如果 onTouch 返回true,表示消费事件,就不会向下传递,就不会执行 onClick,只会执行自己的 down、move(多个move)、up事件;
如果 onTouch 返回false,执行 down、move(多个move)、up事件,最后执行 onClick;

下边通过 View的 dispatchTouchEvent 源码 进行分析 View的事件分发;

3. dispatchTouchEvent源码分析


前提知识:

只要触摸任何一个控件,就一定会调用该控件的 dispatchTouchEvent,如果该控件没有,就一路向上查找,直到找到它父类的 dispatchTouchEvent方法然后调用,比如Button如下:


图片.png
1>:首先看 View 的 dispatchTouchEvent方法如下:
public boolean dispatchTouchEvent(MotionEvent event) {  
    // 存放所有的 listener信息
    ListenerInfo li = mListenerInfo;

    // mOnTouchListener :只要设置 setOnTouchListener()之后,就不会 null;
    // mViewFlags & ENABLED_MASK:只要 该控件是可点击的,这个就是 true;
    // 主要是mOnTouchListener.onTouch(this, event),这个调用 的是  setOnTouchListener 
    // 的 onTouche() 方法,只要 onTouch() 返回 false,就会执行下边的 onTouchEvent(event)
    if (li != null && mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}


static class ListenerInfo {
        protected OnFocusChangeListener mOnFocusChangeListener;

        private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

        protected OnScrollChangeListener mOnScrollChangeListener;

        private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

        public OnClickListener mOnClickListener;
        protected OnLongClickListener mOnLongClickListener;

        protected OnContextClickListener mOnContextClickListener;
        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;

        private OnHoverListener mOnHoverListener;

        private OnGenericMotionListener mOnGenericMotionListener;

        private OnDragListener mOnDragListener;

        private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

        OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
    }

从 dispatchTouchEvent 方法中可知:首先是if判断,如果 mOnTouchListener != null、mViewFlags & ENABLED_MASK== ENABLED、mOnTouchListener.onTouch(this, event) 这3个 条件 都为真,就 返回 true,否则 执行 onTouchEvent(event)方法并返回;

第一个条件:mOnTouchListener != null:

public void setOnTouchListener(OnTouchListener l) {  
    mOnTouchListener = l;  
} 

可以看到 mOnTouchListener 在 setOnTouchListener() 方法中被赋值,也就是说 只要给控件 设置 setOnTouchListener后, mOnTouchListener 就 会被赋值;

第二个条件:mViewFlags & ENABLED_MASK== ENABLED:判断当前点击 控件是否是 enable的,Button 默认是 enable的,ImageView 、TextView不是,所以这个 条件是 true;

第三个条件:mOnTouchListener.onTouch(this, event):

    public interface OnTouchListener {
        boolean onTouch(View v, MotionEvent event);
    }

调用的就是 setOnTouchListener中的 onTouch() 方法,可以看到:
如果 onTouch 返回 true,那么 这3个条件 都为 true,从而整个方法返回 true;
如果 onTouch 返回 false,就 执行下边的 onTouchEvent() 方法;

从这3个条件可知:
前两个条件肯定都为 true,所以 在 dispatchTouchEvent方法中最先执行 setOnTouchListener的onTouch() 方法, 优先级顺序: onTouch > onClick;
如果 onTouch 返回 true,dispatchTouchEvent() 整个方法就 返回 true,不会往下执行,onClick 就不会执行;

onTouchEvent源码如下:

public boolean onTouchEvent(MotionEvent event) {

        // 此处省略一些代码
        ......

        // 从这里可知:
        // 如果该 控件是可点击的,比如 Button ,就会进入 switch 判断,在 Action_Up的 case 语句中,
        // 在 经过一系列判断后,会进入到  performClick()方法;
        // 不管当前 的 action 是什么,在 switch 语句外层,都会 返回 true,

        // 如果该 控件 不是可点击的,比如 ImageView、TextView,就不会进入 if判断,更不会进入 switch 语句,
        // 直接在  最外层的  if语句 返回 false

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {

            switch (action) {
                case MotionEvent.ACTION_UP:

                            if (!focusTaken) {
                                // 此处省略一些判断条件
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                    break;

                case MotionEvent.ACTION_DOWN:
                // 此处省略 down 的一些代码
                    break;

                case MotionEvent.ACTION_CANCEL:
                // 此处省略 cancel 的一些代码
                    break;

                case MotionEvent.ACTION_MOVE:
                // 此处省略 move 的一些代码
                break;
          }
            return true;
        }
        return false;
    }

performClick()源码如下:

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

如果 mOnClickListener 不为 null,就会 调用 它的 onClick() 方法,mOnClickListener在 setOnClickListener中赋值的:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

也就是说 只要调用 setOnClickListener时,就会给 mOnClickListener 赋值,只要 Button被点击,就会调用 performClick() 中的 onClick() 方法;

分析Button:
在 View的 dispatchTouchEvent()方法中,对于 那3个条件,第三个条件的 setOnTouchListener中的 onTouch()如果返回 false,此时进入 onTouchEvent()方法,这个方法中,因为 Button 是可以点击的,所以就会 进入到 onTouchEvent()的 if (((viewFlags & CLICKABLE) == CLICKABLE () 语句,会发现 不管当前 action 是什么,都会返回true,这个是系统帮我们返回的;
分析ImageView:
给 ImageView 设置 setOnTouchListener(),然后给 它的 onTouch()返回 false,此时进入 onTouchEvent()方法,因为 ImageView 是不可点击的,所以就不会进入 onTouchEvent()的 if (((viewFlags & CLICKABLE) == CLICKABLE () 语句,直接 在 这个 if() 语句 最外层就返回 false

4. 结论


1. touch事件层级传递,就是给控件设置 setOnTouchListener():
如果给 控件 设置 setOnTouchListener(),就会 触发一系列的 down、move、up事件,如果 down 中返回false,后边的 move、up等一系列事件均不会执行,意思就是 要想触发后边的 某个 move 或者 up事件执行,前边的 down事件就要返回true;

2. onTouch() 与 onTouchEvent() 区别,如何使用?:
这两个方法 都是 在 View 的 dispatchTouchEvent() 方法中调用的 , 这里的 onTouch() 其实就是 dispatchTouchEvent() 中的 第三个条件;
优先级: setOnTouchListener 的onTouch > onTouchEvent();

如果 onTouch() 返回 true,表示消费事件,那 3个 条件 都为 true,就不会执行下边的 onTouchEvent();

onTouch 要执行的条件有2个:
第一:mOnTouchListener不为null,意思就是 给 该 控件 设置了 setOnTouchListener();
第二:该控件要是 可点击的,就是 enable的;

如果 点击控件是 非enable的,setOnTouchListener的 onTouch()不会执行,比如ImageView、TextView等, 对于 这类控件,如果想监听它的 onTouch事件,就 需要在 该控件中重写 onTouchEvent方法 来实现;

比如 ImageView,想监听它的 onTouch事件,有2种方式:

  1. onTouch方法返回 true;
  2. 在布局中给 ImageView 增加 android:clickable="true"的属性;
上一篇下一篇

猜你喜欢

热点阅读