View 中的事件消息传递,是android的一个重点和难点,我们只有掌握了它,才能更好的理解view,写出自己比较满意的自定义控件,解决控件嵌套时产生的滑动冲突和点击事件失效问题。
我们知道 View 是所有控件的基类,是祖师爷级的存在,我们从它入手,看看它里面的有关事件的方法 dispatchTouchEvent(MotionEvent event) 、 onTouchEvent(MotionEvent event)
、setOnTouchListener(OnTouchListener l) 、 setOnClickListener(OnClickListener l) 、 setOnLongClickListener(OnLongClickListener l) 等,我们先写个简单的demo,看一下
public class TouTestView extends View {
private final static String TAG = "TouTestView";
public TouTestView(Context context) {
this(context, null);
}
public TouTestView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouTestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initLister();
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "dispatchTouchEvent: " + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: " + event.getAction() );
return super.onTouchEvent(event);
}
private void initLister() {
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouchListener: " + event.getAction() + " " + isClickable() +" " + isEnabled());
return false;
}
});
Log.e(TAG, "setOnClickListener" + " " + isClickable() +" " + isEnabled());
}
}
把该控件放入xml布局中,我们进入该页面,发现打印了log,
E/TouTestView: setOnClickListener isClickable() false isEnabled() true
这个是 initLister() 方法中的日志,显示默认view中 默认是无点击, enabled 属性时 true,先打印这个log,放在这里,往下面分析。我们知道,View 的事件分发入口是dispatchTouchEvent(MotionEvent event) 方法,消费事件的有 onTouchEvent(MotionEvent event) 、OnTouchListener 回调,甚至还有 点击事件和长点击事件,这里先分析前面三个方法,当view接收到一个事件,调用 dispatchTouchEvent(MotionEvent event) 方法,我们看一下简化的源码
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
onFilterTouchEventForSecurity(MotionEvent event) 方法默认是true, 除非设置view的布局属性 filterTouchesWhenObscured 改变 mViewFlags 的值,同时event返回的Flags 值与FLAG_WINDOW_IS_OBSCURED 运算后不为0,正常情况下, onFilterTouchEventForSecurity() 返回值为 true。 我们注意看一下 if 里面的代码,先对 mListenerInfo做一个非空判断,
mListenerInfo是什么呢? ListenerInfo 是个静态内部类,存储各种点击和滑动事件的回调,比如 setOnClickListener 点击事件等
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
我们的事件回调,都在它里面存储着,这时候会先把 mOnTouchListener 取出来,看看是否设置了 setOnTouchListener(OnTouchListener l) 方法,注意下一步,做了个值校验(mViewFlags & ENABLED_MASK) == ENABLED, 只有这一步通过了,才会执行 li.mOnTouchListener.onTouch(this, event) 设置的触摸事件,onTouch(this, event) 如果返回了true,result = true;如果 onTouch(this, event) 返回值为false,没有消费,则 result 值不变,依然为 result = false。 下一步,只有result = false时才会执行onTouchEvent(event)方法,如果 onTouchEvent(event) 返回值为 true,则 dispatchTouchEvent(MotionEvent event) 接收到的值为 true,说明焦点触摸事件被消费了,如果onTouchEvent(event) 返回值为
false,则 dispatchTouchEvent(MotionEvent event) 接收到的值为 false,事件未消费。分析到这,我们知道了,一个view,如果设置了setOnTouchListener(OnTouchListener l) ,也重写了 onTouchEvent(event) 方法,它会先执行 OnTouchListener, 如果OnTouchListener回调返回值为false,才会执行 onTouchEvent(event),这里要注意一点小细节,就是
(mViewFlags & ENABLED_MASK) == ENABLED 这个校验,我们在开始的时候打印了个log日志,打印 isClickable() 和 isEnabled(),看isEnabled()源码
public boolean isEnabled() {
return (mViewFlags & ENABLED_MASK) == ENABLED;
}
发现 (mViewFlags & ENABLED_MASK) == ENABLED 这个校验和 isEnabled() 方法代码一样,这也就是说,只有 isEnabled() 为true,才会执行 OnTouchListener 的回调,否则会直接执行 onTouchEvent(event) 方法,这是个小细节,注意一下。
按照我们上面控件里的代码,我们点击了一下控件,打印日志如下
E/TouTestView: dispatchTouchEvent: 0
E/TouTestView: onTouchListener: 0 false true
E/TouTestView: onTouchEvent: 0
先执行 dispatchTouchEvent(MotionEvent event) ,然后 onTouch(View v, MotionEvent event) ,最后 onTouchEvent(MotionEvent event),我们看到打印的action值为0,
先来看几个主要的值,
public static final int ACTION_DOWN = 0; 初次接触到屏幕 时触发。
public static final int ACTION_UP = 1; 离开屏幕 时触发。
public static final int ACTION_MOVE = 2; 在屏幕上滑动 时触发,会多次触发。
public static final int ACTION_CANCEL = 3; 被上层拦截 时触发。
public static final int ACTION_OUTSIDE = 4; 不在控件区域 时触发。
这是我们再次点击,打印的还是这几个值,我们在控件上滑动,打印的日志还是这几个,这是怎么回事呢?如果我们把 onTouch(View v, MotionEvent event) 返回值改为 true,试试?
private void initLister() {
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouchListener: " + event.getAction() );
return true;
}
});
}
点击,打印为
E/TouTestView: dispatchTouchEvent: 0
E/TouTestView: onTouchListener: 0
E/TouTestView: dispatchTouchEvent: 1
E/TouTestView: onTouchListener: 1
如果滑动呢,再试一下
E/TouTestView: dispatchTouchEvent: 0
E/TouTestView: onTouchListener: 0
E/TouTestView: dispatchTouchEvent: 2
E/TouTestView: onTouchListener: 2
E/TouTestView: dispatchTouchEvent: 2
E/TouTestView: onTouchListener: 2
E/TouTestView: dispatchTouchEvent: 2
E/TouTestView: onTouchListener: 2
E/TouTestView: dispatchTouchEvent: 1
E/TouTestView: onTouchListener: 1
我们知道,0代表 按下,1代表抬起,2代表滑动,这次是正常的,那为什么之前不行呢?把代码还原到之前的样式,onTouch(View v, MotionEvent event) 返回为false,修改onTouchEvent(MotionEvent event) 里面的打印代码,
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean is = super.onTouchEvent(event);
Log.e(TAG, "onTouchEvent: " + event.getAction() +" " + is);
return is;
}
按下,滑动,打印日志为
E/TouTestView: dispatchTouchEvent: 0
E/TouTestView: onTouchListener: 0
E/TouTestView: onTouchEvent: 0 false
看来down的时候 onTouchEvent(MotionEvent event) 里面返回的是 false,所以导致了没有后面的move和up事件了,莫非 View 的 onTouchEvent(MotionEvent event) 返回值为false吗?我们看一下它的简略源码
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
...
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
...
break;
case MotionEvent.ACTION_DOWN:
...
setPressed(true, x, y);
checkForLongClick(0);
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
break;
}
return true;
}
return false;
}
这个是简化过后的代码,一些细节被删除,我们主要看剩余的这一部分。 我们看到if代码里,这里如果没进去,则直接返回了return false,我们看看之前的触发事件只有down事件很大可能是因为没走进if的判断语句。我们看看if判断语句是什么:
(viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE
仔细看,会发现一个问题,这不就是变相的
public boolean isClickable() {
return (mViewFlags & CLICKABLE) == CLICKABLE;
}
public boolean isLongClickable() {
return (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
}
public boolean isContextClickable() {
return (mViewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
}
上面的if语句中的判断条件,其实就是这三个方法的判断,文章的开头,我们打印了 isClickable() 为 false,此时如果打印后面两个方法,也都是false,怎么才能改变它的值呢?我们重新看 setOnClickListener(@Nullable OnClickListener l) 、 setOnLongClickListener(@Nullable OnLongClickListener l) 事件,看看方法中,会与运算,判断有没有这个值,如果没有,就赋值,以点击事件为例
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setClickable(boolean clickable) {
setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
}
也就是说,如果我们给view设置一个点击事件的回调,那么 isClickable() 为 true,同理,也适用于 isLongClickable() 和 isContextClickable()。isLongClickable() 是长按点击事件,这个好理解,isContextClickable() 应该是插入外部设备,比如鼠标,是否设置它的回调点击事件。在这,重点关注 isClickable() 和 isLongClickable()。
给view添加点击事件和长按点击事件
private void initLister() {
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouchListener: " + event.getAction() );
return false;
}
});
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "onClick: ");
}
});
setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.e(TAG, "onLongClick: ");
return false;
}
});
}
这时候 onTouchEvent(MotionEvent event) 就可以走到if的判断语句里面了,我们整体看一下,发现只要走到if里面,不管里面怎么判断,switch 判断后,返回的结果必然是returntrue; 也就是说,我们按下后,onTouchEvent(MotionEvent event) 必然是返回 true,我们先不分析代码,看一下打印的日志,点击一下控件,打印结果为
E/TouTestView: dispatchTouchEvent: 0
E/TouTestView: onTouchListener: 0
E/TouTestView: onTouchEvent: 0
E/TouTestView: dispatchTouchEvent: 1
E/TouTestView: onTouchListener: 1
E/TouTestView: onTouchEvent: 1
E/TouTestView: onClick:
我们发现有 down 事件,也有 up 事件,也有点击事件;如果我们在控件上滑动一下,肯定也有 move 事件,这个大家可以自己试一下。我们这时候继续分析代码,首先是Down事件MotionEvent.ACTION_DOWN 中,我们设置了长按点击事件 checkForLongClick(0); CheckForLongPress 是个 Runnable,把长按点击事件的回调包了一层,然后延迟500毫秒执行,可以理解为Handler的延迟执行runnable操作即可,这就是down中做的事情; MotionEvent.ACTION_MOVE 中,如果滑动达到一定的标准,并且在500毫秒内,runnable 还没被执行,这时候就会把 runnable 的长按点击事件回到给取消掉,就不会触发长按事件; MotionEvent.ACTION_CANCEL 就简单了,只要是在时间内,取消长按的runnable,同上;重点看看 MotionEvent.ACTION_UP ,手指抬起时,会有个if判断,if判断里面是view点击事件的回调,PerformClick 也是个 Runnable,最终也会执行performClick()
public boolean performClick() {
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;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
原来我们的点击事件是在UP的时候触发的,但仔细看,触发之前有个if (!mHasPerformedLongPress && !mIgnoreNextUpEvent)判断, mIgnoreNextUpEvent 默认为false,这个可以先不管,看看 mHasPerformedLongPress,意思是是否已经消费了长按点击事件,默认是false,我们看看它是哪里赋值的。从前面的action_down中可以看到,如果500毫秒后执行了长按点击事件,会执行 CheckForLongPress 类里的 run() 方法,
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
public boolean performLongClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
handled = showContextMenu();
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
performLongClick() 在正常情况下可以直接简化为 onLongClick(View v) 方法的返回值,也就是说 mHasPerformedLongPress 对应的是 onLongClick(View v)返回值,如果返回值为false,则 mHasPerformedLongPress = false,同理,如果onLongClick(View v)返回为true,则 mHasPerformedLongPress = true。我们再次看up时里面的代码,就明白 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) 的意思了,mHasPerformedLongPress 会在 MotionEvent.ACTION_DOWN 和 MotionEvent.ACTION_CANCEL 或 checkForLongClick(int delayOffset) 时候重新置为false,mHasPerformedLongPress 的作用域更接近一个焦点事件,down 、move 、up 范围内。如果长按点击事件 onLongClick(View v) 返回为false,onClick(View v) 在长按松手时会被执行;反之,则不会。如果按下了又move了,关键是看 onLongClick(View v)有没有执行,才能决定 onClick(View v) 是否执行。
总结,view中先执行 dispatchTouchEvent(MotionEvent event),接着是设置的触摸回调事件 setOnTouchListener(),如果它的 onTouch(View v, MotionEvent event) 方法返回false,则执行 view 本身的 onTouchEvent(MotionEvent event) 方法,setOnLongClickListener() 长按点击事件时Down时就开始准备了,setOnClickListener() 点击事件是在Up的时候执行的。如果 ACTION_DOWN 的时候,onTouch(View v, MotionEvent event) 或 onTouchEvent(MotionEvent event) 方法返回了false,导致 dispatchTouchEvent(MotionEvent event) 返回的值也是false,那么抱歉,不会有后续的 ACTION_MOVE 和 ACTION_UP 事件了,只有 MotionEvent.ACTION_DOWN 为true时,才会有后面的一系列事件传递。
关于 (viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE
的值,View 、 TextView 、 ImageView 他们的默认值都是 false, 而 Button 、 ImageButton 的默认值为 true,所以如果想让焦点能走个全程,则可以使用 Button 、 ImageButton代替TextView 、 ImageView,或者直接给 TextView 、 ImageView 设置个setOnClickListener()点击事件就可以了。