【Kotlin】关于Android事件传递的整理

关于事件传递的流程,已经有很多大神介绍过了,我在使用的过程中,也遇到了一些问题,在此整理一下,相信有不少同学也有遇到我这样的问题。

问题一:为什么我的onTouchEvent方法只响应了MotionEvent.ACTION_DOWN动作

百度或者Google一搜有一大把这样问题。其根本原因是你的MotionEvent.ACTION_DOWN行为没有被消费掉(没有return true),onTouchEvent默认MotionEvent.ACTION_DOWN为一个触摸事件的开始,如果MotionEvent.ACTION_DOWN没有做处理,则后面一系列行为将都不被响应。

举一个例子,有一个嵌套布局

  • A : ViewGroup
  • B : ViewGroup
  • C : View

注意,只有ViewGrouponInterceptTouchEvent方法,View是没有onInterceptTouchEvent方法的,因为View不能再有子View,不涉及到事件传递

ViewA->ViewB->ViewC

当我们手指触摸C控件的时候,事件的传递过程为

  1. A - dispatchTouchEvent
  2. A - onInterceptTouchEvent
  3. B - dispatchTouchEvent
  4. B - onInterceptTouchEvent
  5. C - dispatchTouchEvent
  6. C - onTouchEvent
  7. B - onTouchEvent
  8. A - onTouchEvent

以上为 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 均未重写的情况

默认情况下,你的ACTION_DOWN是没有被消费掉的,super.onTouchEvent(event)会返回false,比较常见的,有不少人会问,我手指触摸了C控件,但是为什么ACTION_MOVEACTION_UP没有响应呢,如果想要监听ACTION_MOVEACTION_UP,我们只需要重写ConTouchEvent方法,在接收到MotionEvent.ACTION_DOWN的时候返回true,就代表这个触摸事件交给C处理了。

那么事件传递的过程将变成:

  1. A - dispatchTouchEvent
  2. A - onInterceptTouchEvent
  3. B - dispatchTouchEvent
  4. B - onInterceptTouchEvent
  5. C - dispatchTouchEvent
  6. C - onTouchEvent

就没有B、A 的onTouchEvent方法什么事了

问题二:手指在C控件上按下,我想要横着滑动的时候交给控件C处理,竖着滑动的时候交给B处理该如何实现

这里就涉及到事件的拦截处理,就轮到onInterceptTouchEvent方法大显身手的时候了。

首先聊一聊思路,我们都知道,事件的分发是由外到内的,即当我们在C控件上按下的时候,事件是先传到A上,A传给BB再传给C(A -> B -> C)。事件处理是由内到外的,即C先处理,处理完再交给BB处理完再交给A(C -> B -> A)。当前的需求涉及到B和C来处理滑动状态,那么,我们就可以在滑动事件分发到B的时候判断一下,当前的滑动是横向滑动的还是纵向滑动的:

  1. 如果是横向滑动的,那么onInterceptTouchEvent方法不做拦截,允许事件传到CC接收到事件后,在onTouchEvent中将事件消费掉,这样B就不需要进行处理(不会接收到onTouchEvent的响应)。
  2. 如果是纵向滑动的,那么onInterceptTouchEvent就可以返回true,将事件拦截下来(不需要交给C处理,C就不会收的onTouchEvent的响应),B自己在onTouchEvent中将事件消费掉。

举个栗子

现在我们想实现这样一个功能,在页面上下滑动可以翻页,在控件上左右滑动可以拦截翻页动作

这里写图片描述

思路:

首先要写布局,自定义个 View 继承 ViewGroup, 然后开始我们的xml布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<com.kongqw.view.ScrollViewPager
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFEEEEEE">
<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:text="我是第 0 页内容"
android:textSize="20sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/tv1"
android:layout_centerInParent="true"
android:src="@mipmap/ic_launcher" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFBBBBBB">
<Button
android:id="@+id/btn_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:text="我是第 1 页内容"
android:textSize="20sp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF999999">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="我是第 2 页内容"
android:textSize="20sp" />
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#FF888888" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF777777"
android:gravity="center"
android:text="我是第 3 页内容"
android:textSize="20sp" />
</com.kongqw.view.ScrollViewPager>

写完布局我们要重写onMeasureonLayout方法,开始对布局测量、排布。
我们这里是纵向排布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 测量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
for (index in 0 until childCount) {
measureChild(getChildAt(index), widthMeasureSpec, heightMeasureSpec)
}
}
/**
* 布局 纵向布局
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var top = 0
var bottom = 0
for (index in 0 until childCount) {
val childView = getChildAt(index)
bottom = top + childView.measuredHeight
childView.layout(0, top, childView.measuredWidth, bottom)
top = bottom
}
mBottomBorder = bottom
}

写到这里运行程序,应该是可以看得到第一个页面了,但是滑动没有任何反应,我们需要重写onTouchEvent方法,让View可以根据手指的滑动而滚动,这里需要监听event的ACTION_DOWNACTION_MOVEACTION_UPACTION_CANCEL状态,通过ACTION_MOVE记录手指每次纵向移动的距离,在监听到ACTION_UPACTION_CANCEL的时候,判断滑动位置,然后滚动到哪个页面。这样,一个简单的页面滑动就实现了。

scrollBy、scrollTo

提到滚动,就不得不提到两个方法,scrollByscrollTo,值得注意的是,通常我们一种面向对象的思想,想让谁滚动,谁就调用自己的scroll就行了,但是,这里的scrollByscrollTo针对的是自己子View的运动,说白话了,就是老爸要管理儿子运动的。
scrollBy是控制一次滚动多少,scrollTo是控制滚动到哪里。

我们这里自定义了ScrollViewPager,页面内容都写在ScrollViewPager内部,所以,我们控制滚动,就是在控制ScrollViewPager内部的子View滚动,所以,直接调用scrollByscrollTo即可。

Scroller

现在滑动没有问题了,但是手指一松开,View 会瞬间移动到指定位置,体验不太好,我们想要View的滚动有一个顺滑的过程,这就又不得不再提到一个Scroller类,他可以帮助我们完成动画的平缓过度。

通过ScrollerstartScroll来初始化运动的起始位置、结束位置和运动时间,然后调用invalidate()开始处理,这里会一直回调ViewcomputeScroll方法,是一个空方法,需要我们自己在这里自己依据ScrollercomputeScrollOffset()判断移动是否完成,然后调用scrollTo来完成平滑移动处理。

到这里,View就可以实现上下滑动,并平滑过渡了,接下来来处理事件的分发问题。
我们想要实现的目标是,当我们上下滑动的时候,可以滚动页面,要将事件拦截。
当我们在子控件上左右滑动的时候,可以将事件先交给子控件处理。那么我们就可以重写ScrollViewPageronInterceptTouchEvent方法,在ACTION_MOVE的时候判断滑动方向,如果是上下滑动,则返回true,将事件拦截即可。

下面是自定义ScrollViewPager源码(Kotlin代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package com.kongqw.view.ScrollViewPager
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.widget.Scroller
/**
* Created by Kongqw on 2018/3/29.
* ScrollViewPager
*/
class ScrollViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
/**
* 手指按下位置
*/
private var mDownX = 0.0F
private var mDownY = 0.0F
/**
* 手指移动后的位置
*/
private var mMoveX = 0.0F
private var mMoveY = 0.0F
/**
* 手指最后移动位置
*/
private var mLastX = 0.0F
private var mLastY = 0.0F
/**
* View 上边界
*/
private val mTopBorder = 0
/**
* View 下边界
*/
private var mBottomBorder = 0
/**
* 滑动处理
*/
private val mScroller: Scroller = Scroller(context)
/**
* 事件是否拦截
*/
private var isIntercept = false
/**
* 最小移动单位
*/
private val mScaledtouchslop = ViewConfiguration.get(context).scaledTouchSlop
companion object {
private val TAG: String = ScrollViewPager::class.java.simpleName
}
/**
* 测量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
for (index in 0 until childCount) {
Log.i(TAG, "测量第 $index 个控件大小")
measureChild(getChildAt(index), widthMeasureSpec, heightMeasureSpec)
}
}
/**
* 布局 纵向布局
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var top = 0
var bottom = 0
for (index in 0 until childCount) {
val childView = getChildAt(index)
bottom = top + childView.measuredHeight
Log.i(TAG, "布局第几个View [0, ${top}, ${childView.measuredWidth}, ${bottom}]")
childView.layout(0, top, childView.measuredWidth, bottom)
top = bottom
}
mBottomBorder = bottom
Log.i(TAG, "View 高度 $height")
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
val touchX = ev?.rawX
val touchY = ev?.rawY
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
mDownX = touchX!!
mDownY = touchY!!
isIntercept = false
}
MotionEvent.ACTION_MOVE -> {
mMoveX = touchX!!
mMoveY = touchY!!
val dx = Math.abs(mMoveX - mDownX)
val dy = Math.abs(mMoveY - mDownY)
// if(dy > dx){
// isIntercept = true
// }
isIntercept = (dy > dx)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isIntercept = false
}
}
mLastX = touchX!!
mLastY = touchY!!
return isIntercept
// return super.onInterceptTouchEvent(ev)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchY = event?.rawY
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.i(TAG, "ACTION_DOWN")
// 获取手指按下的位置
mDownY = touchY!!
return true
}
MotionEvent.ACTION_MOVE -> {
Log.i(TAG, "ACTION_MOVE")
// 获取手指移动后的位置
mMoveY = touchY!!
// 计算手指在Y轴移动距离
// val dY = Math.abs(mLastY - mDownY)
val dY = mLastY - mMoveY
when {
scrollY < mTopBorder -> {
// 向下已经划出上边界
Log.i(TAG, "向下已经划出上边界")
scrollBy(0, dY.toInt() / 5)
}
scrollY + height > mBottomBorder -> {
// 向上已经划出下边界
Log.i(TAG, "向上已经划出下边界")
scrollBy(0, dY.toInt() / 5)
}
else -> {
// 移动
scrollBy(0, dY.toInt())
}
}
// 刷新页面
invalidate()
mLastY = mMoveY
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
Log.i(TAG, "ACTION_UP View 纵向滑动了 $scrollY")
when {
scrollY <= mTopBorder -> {
// 向下已经划出上边界
Log.i(TAG, "向下已经划出上边界")
mScroller.startScroll(0, scrollY, 0, -scrollY)
invalidate()
}
scrollY + height >= mBottomBorder -> {
// 向上已经划出下边界
Log.i(TAG, "向上已经划出下边界")
val dy = mBottomBorder - scrollY - height
mScroller.startScroll(0, scrollY, 0, dy)
invalidate()
}
else -> {
// 移动
val page = computePageByScrollY(scrollY)
Log.i(TAG, "滑动到第 $page 页")
mScroller.startScroll(0, scrollY, 0, height * page - scrollY)
invalidate()
}
}
}
}
mLastY = touchY!!
return super.onTouchEvent(event)
}
override fun setOnTouchListener(l: OnTouchListener?) {
super.setOnTouchListener(l)
}
/**
* 重写平滑移动处理逻辑
*/
override fun computeScroll() {
super.computeScroll()
if (mScroller.computeScrollOffset()) {
// 滑动还没有结束
scrollTo(mScroller.currX, mScroller.currY)
invalidate()
}
}
/**
* 根据Y轴滑动距离判断应该显示哪一页
*/
private fun computePageByScrollY(scrollY: Int = 0): Int {
val v = scrollY / (height / 2)
Log.i(TAG, "computePageByScrollY scrollY = $scrollY v = $v")
return when (v) {
0 -> 0
1, 2 -> 1
3, 4 -> 2
5, 6, 7 -> 3
else -> 0
}
}
}
坚持原创技术分享,您的支持将鼓励我继续创作!