Android事件分发机制 & 面试解析
2022-02-10 本文已影响0人
JianLee
先总结一下事件分发机制的流程
- 事件分发从Action_Down开始,最初由Activity的dispatchTouchEvent()方法接收,不拦截不中断的正常分发流程:Activity的disPatchTouchEvent()方法到PhoneWindow的superDispatchTouchEvent方法,再到DecorView的superDispatchTouchEvent方法,再到ViewGroup的dispatchTouchEvent方法,在ViewGroup的dispatchTouchEvent方法中判断是否拦截,若拦截调用ViewGroup的onTouchEvent方法,该ViewGroup消费掉;若不拦截,该ViewGroup遍历子View根据点击的位置等条件判断是否为接收事件的子View,是,则分发给该子View的dispatchTouchEvent()方法,然后会调用View的onTouchEvent方法,在onTouchEvent方法中会判断该子View是否可点击,是,则事件最终传递到View的onClick方法消费;否则,事件返回向上传递,直到消费或者终止。
- 在dispatchTouchEvent()方法中返回true或者false,事件不向下传递,只用调用super.dispatchTouchEvent方法,事件才会向下传递。
- 在onTouchEvent()方法中返回true,事件在该方法中消费,不会向下或者向上传递;返回super.onTouchEvent方法,将会调用View onTouchEvent方法,判断长按事件和点击事件的执行条件存不存在,存在则会在点击事件中消费。
- 在onInterceptTouchEvent()方法中返回true表示拦截事件,事件可能会在该ViewGroup中消费掉;返回false表示事件继续往下传递。
ViewGroup 默认拦截事件吗?
答:默认不拦截任何事件,onInterceptTouchEvent返回的是false。
一旦有事件传递给view,view的onTouchEvent一定会被调用吗?
答:是的,因为view 本身没有onInterceptTouchEvent方法,所以只要事件来到view这里 就一定会走onTouchEvent方法。
并且默认都是消耗掉,返回true的。除非这个view是不可点击的,所谓不可点击就是clickable和longgclikable同时为fale
Button的clickable就是true 但是textview是false。
enable是否影响view的onTouchEvent返回值?
答:不影响,只要clickable和longClickable有一个为真,那么onTouchEvent就返回true。
requestDisallowInterceptTouchEvent 可以在子元素中干扰父元素的事件分发吗?如果可以,是全部都可以干扰吗?
答:肯定可以,但是down事件干扰不了。
dispatchTouchEvent每次都会被调用吗?
答:是的,onInterceptTouchEvent则不会。
滑动冲突问题如何解决 思路是什么?
要解决滑动冲突核心的方法就是2个 外部拦截也就是父亲拦截,另外就是内部拦截,也就是子view拦截法。 学会这2种 基本上所有的滑动冲突都是这2种的变种,而且核心代码思想都一样。
- 外部拦截法:思路就是重写父容器的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因为这和android自身的事件处理机制 逻辑是一模一样的。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
//down事件肯定不能拦截 拦截了后面的就收不到了
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (你的业务需求) {
//如果确定拦截了 就去自己的onTouchEvent里 处理拦截之后的操作和效果 即可了
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//up事件 我们一般都是返回false的 一般父容器都不会拦截他。 因为up是事件的最后一步。这里返回true也没啥意义
//唯一的意义就是因为 父元素 up被拦截。导致子元素 收不到up事件,那子元素 就肯定没有onClick事件触发了,这里的
//小细节 要想明白
intercepted = false;
break;
default:
break;
}
return intercepted;
}
- 内部拦截法:内部拦截法稍微复杂一点,就是事件到来的时候,父容器不管,让子元素自己来决定是否处理。如果消耗了 就最好,没消耗 自然就转给父容器处理了。
子元素代码:
@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:
if (如果父容器需要这个点击事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}//否则的话 就交给自己本身view的onTouchEvent自动处理了
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
父亲容器代码也要修改一下,其实就是保证父亲别拦截down:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
怎么解决 ScrollView 嵌套 ScrollView 后内部 ScrollView 无法滑动的问题?
答:根本原因就是因为用户的滑动操作都被外部 ScrollView 拦截并消费了,导致内部 ScrollView 一直无法响应滑动事件。
这里选择使用内部拦截法来解决问题。首先需要让外部 ScrollView 拦截 ACTION_DOWN 之外的任何事件
class ExternalScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ScrollView(context, attrs, defStyleAttr) {
override fun onInterceptTouchEvent(motionEvent: MotionEvent): Boolean {
val intercepted: Boolean
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
intercepted = false
super.onInterceptTouchEvent(motionEvent)
}
else -> {
intercepted = true
}
}
return intercepted
}
}
内部 ScrollView 判断自身是否还处于可滑动状态,如果滑动到了最顶部还想再往下滑动,或者是滑动到了最底部还想再往上滑动,那么就将事件都交由外部 ScrollView 处理,其它情况都直接拦截并消费掉事件,这样内部 ScrollView 就可以实现内部滑动了
class InsideScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ScrollView(context, attrs, defStyleAttr) {
private var lastX = 0f
private var lastY = 0f
override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
val x = motionEvent.x
val y = motionEvent.y
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
val deltaX = x - lastX
val deltaY = y - lastY
if (abs(deltaX) < abs(deltaY)) { //上下滑动的操作
if (deltaY > 0) { //向下滑动
if (scrollY == 0) { //滑动到顶部了
parent.requestDisallowInterceptTouchEvent(false)
}
} else { //向上滑动
if (height + scrollY >= computeVerticalScrollRange()) { //滑动到底部了
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
MotionEvent.ACTION_UP -> {
}
}
lastX = x
lastY = y
return super.dispatchTouchEvent(motionEvent)
}
}