对事件分发还不太熟悉,且看完博客还一头雾水的朋友,不妨从面试题下手,带着问题去看源码,再由点到面,辅以xmind和流程图,看能否得以突破。
面试题:
1.view和viewgroup的事件分发?(事件分发的原理)
流程如下
2.onTouch和onClick冲突问题?
onTouch和onClick冲突的原因是因为,onTouch设置为true,onClick就不会执行。详细案例和源码分析如下。
2.1案例:
button.setOnTouchListener(this);
button.setOnClickListener(this);
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: ");
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(TAG, "onTouch: ");
//button.performClick();假设加入这个,即便是写true,也会触发onclick.
return true;//关键是这里,如果设置为true则onclick不打印,设置为false,onclick就打印
}
2.2源码分析:
用户触摸屏幕时,将产生Touch事件,Touch事件的相关细节(包括触摸位置,时间等,)而封装的对象,不是Java来做的,而是工作activity的驱动来做的,驱动做完之后,触摸到屏幕时,第一时间会调用到activity的dispatchTouchEvent(),
起点则为Activity的dispatchTouchEvent()
//Activity类
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {//为down时触发
onUserInteraction();//作用:实现屏保功能
//当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
}
if (getWindow().superDispatchTouchEvent(ev)) {//PhoneWindow的superDispatchTouchEvent()着重分析。
return true;
}
return onTouchEvent(ev);
}
//PhoneWindow类
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);//这里是DecorView
}
//DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event); //ViewGroup的dispatchTouchEvent()
}
1 | 流程: |
ViewGroup的DispatchTouchEvent()
//触摸屏幕 不一定是用手指去按手机屏幕,也有可能是通过vysor等辅助功能来触摸
//而ViewGroup的DispatchTouchEvent()里面 前几段代码的判断。
//则是专门针对vysor等辅助功能软件进行判断
//是手指亲自点击 还是用辅助功能点击
//涉及到的就不是Java层的了,而是驱动层
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
//Accessibility就是在判断,是否是辅助功能。
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;//方法最终返回的就是handled.
if (onFilterTouchEventForSecurity(ev)) {
boolean handled = false;//方法最终返回的就是handled.
if (onFilterTouchEventForSecurity(ev)) {//过滤,是否拦截,由requestDisallowInterceptTouchEvent()给flgs赋值。从而决定是true还是false
//下面的判断操作 总体是三个部分
//1. 是否是第一次按下的操作,是的话,重启touch状态,touch是针对单指操作
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);//清理touch的target
resetTouchState();//重置状态
}
// 2.是否进行拦截
final boolean intercepted;
if (!disallowIntercept) {
//disallowIntercept这个值决定了 拦截还是不拦截,而赋值操作,则是在requestDisallowInterceptTouchEvent()进行赋值
//只有viewgroup才可以拦截,view是没有拦截的。view只有分发和处理
intercepted = onInterceptTouchEvent(ev);
//是否取消操作,取消就为true,默认是为false,代表手指没有到屏幕外面
final boolean canceled = resetCancelNextUpFlag(this)
if (!canceled && !intercepted) {//第一次触发,实际上在这里,默认都是false,因为手指没有到屏幕外面,所以canceled为false,默认没有被拦截,也为false。实际上是进入到这里
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
//3.以下则是属于对多控件,对子view按照层级进行排列
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();//这里则是将子view按照层级来保存
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
//倒序遍历,获取子控件
//省略部分业务代码(提取当前的view和按下时间以及坐标等
)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//这里的dispatchTransformedTouchEvent才是核心代码。内部分发给view的dispatchTouchEvent()
return handled;
}
dispatchTransformedTouchEvent方法 源码分析:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);//因为child不为空,则走到这里,child实际上就是view。
}
event.setAction(oldAction);
return handled;
}
也就是说,从现在为止,调用顺序则为1
2
3
4
5
6
7流程:
起点:底层驱动
==>Activity的dispatchTouchEvent()
==>PhoneWindow的superDispatchTouchEvent()
==>DecorView的superDispatchTouchEvent()
==>ViewGroup的superDispatchTouchEvent()(核心分发)
==>View的dispatchTouchEvent()(核心分发)
View的dispatchTouchEvent()的部分重点源码:
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED//所有控件默认都是ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//主要看view调用的onTouch为true还是false
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
//关键是这里是设置true还是false。如果是false,则会走result=true,到!result时,就不会执行ontouchEvent()方法,反之就会走onTouchEvent,不执行OntouchEvent,则不会执行其中的performClick(),就不会执行onclick,所以onClick事件是不执行的。
从这里的源码,则可以回答刚才的现象,为什么设置false就会执行onTouchEvent,设置true则不会执行。
当然 核心点就是performClick()方法,如果在onTouchListener中,直接用view去调用performClick(),那么即便onTouchListener返回true,onclick也会执行。
3.onClick和onLongClick事件能同事发生吗?
案例代码:
MainActivity的代码
View viewById = findViewById(R.id.myview);
viewById.setOnTouchListener(this);
viewById.setOnClickListener(this);
viewById.setOnLongClickListener(this);
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: ");
}
@Override
public boolean onLongClick(View v) {
Log.i(TAG, "onLongClick: ");
return false;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(TAG, "onTouch: ");
return false;
}
自定义view的代码
private static final String TAG = MeView.class.getSimpleName();
public MeView(Context context) {
super(context);
}
public MeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(TAG, "我下发任务了 dispatchTouchEvent :");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "我处理任务 onTouchEvent :");
return super.onTouchEvent(event);
}
可以同时发生,只要onLongClick设置为false就可以同时发生,设置为true的话,onClick就不会同时发生。
通过这个打log来看
onLongClick应该是在onTouchEvent()中的down时执行的
onClick应该是在onTouchEvent()中的up时执行的
onTouchEvent()源码分析:
分别针对down和up的情况进行分析。
//找到up的主要源码
case MotionEvent.ACTION_UP:
if (!post(mPerformClick)) {
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;
}
return result;
}
// 找到down的主要源码。
case MotionEvent.ACTION_DOWN:
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
当然,这其中还有很多流程性的原理。例如 postDelay()方法和removeLongClick()方法
结论 :
1.从down到up,100毫秒内是点击事件 onclick,
超过500毫秒则是长按事件,onLongClick()
2.move时,未离开控件,则将onclick和onLongClick()移除掉
关键代码
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//这里只截取部分onTouchEvent()代码
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
return false;
}
只要是进入到down move up事件,onTouchEvent()return就为true,否则为false。
也就是说
down move up,任意一个return为true,下面就会执行,如果为false,则不会执行。
调用默认的super的话,因为符合CLICKABLE,所以是可以执行。
如果是继承imageview,则不会执行onTouchEvent(),因为imageview默认是不可点击的,必须要设置点击属性才可以
上面只是分析了viewgroup的dispatchTouchEvent和view的dispatchTouchEvent,,那么viewgroup的流程还会继续走,事件则会自己消耗掉。
if (mFirstTouchTarget == null) {
//如果view的dispatchTouchEvent为false,则mFirstTouchTarget为空
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
//事件会自己消费掉
}
核心点在于:
责任链模式
事件分发uml流程图
实际案例bug
典型案例:
1.关于popupwindow的bug。
https://blog.csdn.net/qq402164452/article/details/53353798
2.https://blog.csdn.net/Dota_wy/article/details/77451011
当然 这里还要结合实际的滑动冲突的bug,才能真正理解