Android View事件分发
目录
7. onTouch()和onTouchEvent()的区别
个人对于Android事件分发的知识点总结,相对零散,源码了解的不够多,就没有展开说明。主要总结关于Android中的
View事件是如何分发的?如何传递的?如何消耗的?
1.什么是View事件分发?
将点击事件(MotionEvent)传递到某个具体的View或者ViewGroup处理的过程。事件分发是向下传递的,也就是父到子的顺序,当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件(Touch事件)。Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象。
2.事件的类型
这里的事件指的就是点击事件(MotionEvent),主要分为以下几个类型:
(1)ACTION_DOWN:手指刚碰到屏幕的时候;
(2)ACTION_UP:手指离开屏幕的时候;
(3)ACTION_CANCEL:结束事件的时候(子View处理事件的过程中,父View拦截事件,收回处理权,此时的子View会收到这个事件);
(4)ACTION_MOVE:手指在屏幕上进行滑动的时候;
3.事件的发生
- 在一次屏幕触摸事件中,ACTION_DOWN和ACTION_UP者两个事件是必须的,而ACTION_MOVE视情况而定,如果用户仅是点击了一下屏幕,那么可能只会监测到按下和抬起的动作。
- 通过MotionEvent可以获得事件发生的x和y坐标,getX和getY返回的是相当于当前View左上角的X和Y坐标,getRawX和getRawY返回的是相当于手机屏幕左上角的X和Y坐标
- 事件分发的本质就收将点击事件(MotionEvent)向某个View进行传递并最终得到处理,即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。
- Android事件分发机制的本质是要解决,点击事件由哪个对象发出,经过哪些对象,最终达到哪个对象并最终得到处理。
- 事件分发流程:Native层 --> ViewRootImpl层 --> DecorView层 --> Activity层 --> ViewGroup层 --> View层
- 当一个点击事件发生时,调用顺序如下
- 1.事件最先传到Activity的dispatchTouchEvent()进行事件分发
- 2.调用Window类实现类PhoneWindow的superDispatchTouchEvent()
- 3.调用DecorView的superDispatchTouchEvent()
- 4.最终调用DecorView父类的dispatchTouchEvent(),即ViewGroup的dispatchTouchEvent(),再由ViewGroup传递到View
- 5.在ViewGroup中通过onInterceptTouchEvent()对事件传递是否进行拦截
4.事件分发的方法
Android 的事件分发机制主要涉及 Activity
、ViewGroup
和 View
三者对触摸事件的传递和处理流程。该机制通过三个主要方法进行分发和拦截:
(1)dispatchTouchEvent():分发点击事件;
(2)onTouchEvent():处理点击事件;
(3)onInterceptTouchEvent():拦截某个事件;
4.1 dispatchTouchEvent()
作用对象:Activity,ViewGroup和View 里面的源码有兴趣可以去看看
它会返回一个布尔值。
- 返回true
- 消费事件
- 事件不会往下传递
- 后续事件(Move、Up)会继续分发到该View
- 返回false
- 不消费事件
- 事件往下传递
- 将事件回传给父控件的onTouchEvent()处理
- Activity例外:返回false=消费事件
- 后续事件(Move、Up)会继续分发到该View(与onTouchEvent()区别)
4.2 onTouchEvent()
作用对象:Activity,ViewGroup和View
作用:在dispatchTouchEvent()内部调用,用于处理点击事件。它会返回一个布尔值。
- 返回true
- 自己处理(消费)该事情
- 事件停止传递
- 该事件序列的后续事件(Move、Up)让其处理;
- 返回false(同默认实现:调用父类onTouchEvent())
- 不处理(消费)该事件
- 事件往上传递给父控件的onTouchEvent()处理
- 当前View不再接受此事件列的其他事件(Move、Up);
4.3 onInterceptTouchEvent()
该方法表示是否对事件进行拦截,只有在ViewGroup里才有此方法,代表这个事件是否往下传递
- true-当前ViewGroup希望该事件不再传递给其child,而是希望自己处理。
- false-当前ViewGroup不准备拦截该事件,事件正常向下分发给其child。
当用户进行触摸/点击事件后,他们的执行顺序
dispatchTouchEvent()--->onInterceptTouchEvent()--->onTouchEvent()
一般来说事件是这样传递的:
- 从Activity A---->ViewGroup B--->View C,从上往下调用dispatchTouchEvent()
- 再由View C--->ViewGroup B --->Activity A,从下往上调用onTouchEvent()
5.滑动冲突
当父容器与子 View 都可以滑动时,就会产生滑动冲突。解决 View 之间的滑动冲突的方法分为两种,分别是外部拦截法和内部拦截法
5.1 外部拦截法
即在父容器进行选择性拦截操作,即使用onInterceptTouchEvent方法。
例如对指定方向的滑动进行拦截,当满足条件时,返回true,表示此事件被拦截,不进行事件下发
tips:1.ACTION_DOWN 事件需要返回 false,父容器不能进行拦截,否则根据 View 的事件分发机制,后续的 ACTION_MOVE 与 ACTION_UP 事件都将默认交由父容器进行处理。
2.原则上 ACTION_UP 事件也需要返回 false,如果返回 true,那么子 View 将接收不到 ACTION_UP 事件,子 View 的onClick 事件也无法触发。
例子:实现一个自定义ScrollView
class CustomScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ScrollView(context, attrs) {
private var lastX = 0f
private var lastY = 0f
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
lastX = ev.x
lastY = ev.y
// 不拦截,让子视图接收事件
return false
}
MotionEvent.ACTION_MOVE -> {
val deltaX = Math.abs(ev.x - lastX)
val deltaY = Math.abs(ev.y - lastY)
// 如果 Y 轴移动距离大于 X 轴,则父容器拦截
return deltaY > deltaX
}
}
return super.onInterceptTouchEvent(ev)
}
}
在此实现中,onInterceptTouchEvent
方法会根据手指滑动的方向来决定是否拦截事件。如果纵向滑动距离更大,则 ScrollView
会拦截事件。
5.2内部拦截法
内部拦截法在子视图(如 RecyclerView
)中处理,通过对子视图的 onTouchEvent
进行控制,让子视图自己决定是否请求父容器不要拦截。
例子:自定义一个RecycleView
class CustomRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RecyclerView(context, attrs) {
private var lastX = 0f
private var lastY = 0f
override fun onTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
lastX = ev.x
lastY = ev.y
// 请求父容器不要拦截事件
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
val deltaX = Math.abs(ev.x - lastX)
val deltaY = Math.abs(ev.y - lastY)
// 当检测到横向滑动时,允许父容器拦截
if (deltaX > deltaY) {
parent.requestDisallowInterceptTouchEvent(false)
} else {
parent.requestDisallowInterceptTouchEvent(true)
}
}
}
return super.onTouchEvent(ev)
}
}
在这个示例中,RecyclerView
会在接收到横向滑动时调用 parent.requestDisallowInterceptTouchEvent(false)
,使父容器可以拦截事件;而在纵向滑动时调用 parent.requestDisallowInterceptTouchEvent(true)
,让父容器不拦截。
总结
- 外部拦截法适合在父容器中根据滑动方向决定是否拦截事件。
- 内部拦截法让子视图根据滑动方向来请求父容器是否拦截事件。
6.onTouch的执行高于onClick
因为每当控件被点击时:
- 如果在回调onTouch()里返回false,就会让dispatchTouchEvent方法返回false,那么就会执行onTouchEvent();如果回调了setOnClickListener()来给控件注册点击事件的话,最后会在performClick()方法里回调onClick()。
- onTouch()返回false(该事件没被onTouch()消费掉) = 执行onTouchEvent() = 执行OnClick()
- 如果在回调onTouch()里返回true,就会让dispatchTouchEvent方法返回true,那么将不会执行onTouchEvent(),即onClick()也不会执行;
- onTouch()返回true(该事件被onTouch()消费掉) = dispatchTouchEvent()返回true(不会再继续向下传递) = 不会执行onTouchEvent() = 不会执行OnClick()
- onTouch->onTouchEvent->onClick
7. onTouch()和onTouchEvent()的区别
- 这两个方法都是在View的dispatchTouchEvent中调用,但onTouch优先于onTouchEvent执行。
- 如果在onTouch方法中返回true将事件消费掉,onTouchEvent()将不会再执行。
8.实现View的滑动
- layout:对View进行重新布局定位。在onTouchEvent()方法中获得控件滑动前后的偏移。然后通过layout方法重新设置。
- offsetLeftAndRight和offsetTopAndBottom:系统提供上下/左右同时偏移的API。onTouchEvent()中调用
- LayoutParams: 更改自身布局参数(设置margin或者父容器的padding等等)
- scrollTo/scrollBy: 本质是移动View的内容,需要通过父容器的该方法来滑动当前View
- Scroller: 平滑滑动,通过重载computeScroll(),使用scrollTo/scrollBy完成滑动效果。
- 属性动画: 动画对View进行滑动
9.SurfaceView和View
SurfaceView是从View基类中派生出来的显示类,他和View的区别有:
- View需要在UI线程对画面进行刷新,而SurfaceView可在子线程进行页面的刷新
- View适用于主动更新的情况,而SurfaceView适用于被动更新,如频繁刷新,这是因为如果使用View频繁刷新会阻塞主线程,导致界面卡顿
- SurfaceView在底层已实现双缓冲机制,而View没有,因此SurfaceView更适用于需要频繁刷新、刷新时数据处理量很大的页面
原文地址:https://blog.csdn.net/LoveFHM/article/details/139481107
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!