1.为什么要了解Android事件机制?
背景:我在做Android项目的时候遇到一个Activity->Fragment->ScrollView->Button这样的嵌套关系,当一切都准备就绪,程序启动后点击Button的时候系统异常崩溃了,腾讯Bugly抓到后报以下错误。
java.lang.IllegalArgumentException
Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter e1
不用看了就是传递的参数有问题,要解决这个问题就必须要了解Android的事件分发机制。
2.如何解决。
先让我来看一Android事件分发机制的张图
从上图中可以看出当点击Activity上的某一个View的时候,Android展示了它完整的系统默认事件分发机制Activity->viewGroup->view。
Activity(DispatchTouchEvent)->(VIewGroup)DispatchTouchEvents->(VIewGroup)onInterceptTouchEvent->(view)dispatchTouchEvent->(View)OnTouchEvent
案例1:
新建一个DispatchEvent项目;
新建一个MyLinearLayout
class MyLinearLayout(context:Context): LinearLayout(context) { override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { when(ev?.action) { MotionEvent.ACTION_DOWN->{ Log.d("GroupView_dispatchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{Log.d("GroupView_dispatchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{Log.d("GroupView_dispatchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{ (Log.d("GroupView_dispatchEvent","ACTION_CANCEL")) } } return super.dispatchTouchEvent(ev) } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { when(ev?.action) { MotionEvent.ACTION_DOWN->{Log.d("GroupView_onIntercept","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{Log.d("GroupView_onIntercept","ACTION_MOVE")} MotionEvent.ACTION_UP->{Log.d("GroupView_onIntercept","ACTION_UP")} MotionEvent.ACTION_CANCEL->{Log.d("GroupView_onIntercept","ACTION_CANCEL")} } return super.onInterceptTouchEvent(ev) } override fun onTouchEvent(event: MotionEvent?): Boolean { when(event?.action) { MotionEvent.ACTION_DOWN->{Log.d("GroupView_onTouchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{Log.d("GroupView_onTouchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{Log.d("GroupView_onTouchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{Log.d("GroupView_onTouchEvent","ACTION_CANCEL")} } return super.onTouchEvent(event) } }
2.新建一个MyButton
class MyButton(context:Context): Button(context) { override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { when(ev?.action) { MotionEvent.ACTION_DOWN->{ Log.d("View_dispatchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{ Log.d("View_dispatchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{ Log.d("View_dispatchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{ Log.d("View_dispatchEvent","ACTION_CANCEL") } } return super.dispatchTouchEvent(ev) } override fun onTouchEvent(event: MotionEvent?): Boolean { when(event?.action) { MotionEvent.ACTION_DOWN->{Log.d("View_onTouchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{Log.d("View_onTouchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{Log.d("View_onTouchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{Log.d("View_onTouchEvent","ACTION_CANCEL")} } Log.d("View_onTouchEvent","我是最后的消费者${super.onTouchEvent(event)}") return super.onTouchEvent(event) } }
3.在MainActivity写上如下代码
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) var myLinearLayout = MyLinearLayout(this); var myButton = MyButton(this); myButton.setText("MyButton") var viewGroups:ViewGroup.LayoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT); var viewPar:ViewGroup.LayoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT); myLinearLayout.orientation = LinearLayout.VERTICAL; myLinearLayout.addView(myButton,viewPar); setContentView(myLinearLayout,viewGroups); } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { when(ev?.action) { MotionEvent.ACTION_DOWN->{ Log.d("Activity_dispatchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{ Log.d("Activity_dispatchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{ Log.d("Activity_dispatchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{ Log.d("Activity_dispatchEvent","ACTION_CANCEL") } } return super.dispatchTouchEvent(ev) } override fun onTouchEvent(event: MotionEvent?): Boolean { when(event?.action) { MotionEvent.ACTION_DOWN->{Log.d("Activity_onTouchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{Log.d("Activity_onTouchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{Log.d("Activity_onTouchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{Log.d("Activity_onTouchEvent","ACTION_CANCEL")} } return super.onTouchEvent(event) } }
点击Butuon观察打印信息
09-29 00:20:11.109 8276-8276/com.example.krcm110.myapplication D/Activity_dispatchEvent: ACTION_DOWN
09-29 00:20:11.116 8276-8276/com.example.krcm110.myapplication D/GroupView_dispatchEvent: ACTION_DOWN
09-29 00:20:11.117 8276-8276/com.example.krcm110.myapplication D/GroupView_onIntercept: ACTION_DOWN
09-29 00:20:11.118 8276-8276/com.example.krcm110.myapplication D/View_dispatchEvent: ACTION_DOWN
09-29 00:20:11.118 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: ACTION_DOWN
09-29 00:20:11.151 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: 我是最后的消费者true
09-29 00:20:11.154 8276-8276/com.example.krcm110.myapplication D/Activity_dispatchEvent: ACTION_UP
09-29 00:20:11.154 8276-8276/com.example.krcm110.myapplication D/GroupView_dispatchEvent: ACTION_UP
09-29 00:20:11.155 8276-8276/com.example.krcm110.myapplication D/GroupView_onIntercept: ACTION_UP
09-29 00:20:11.155 8276-8276/com.example.krcm110.myapplication D/View_dispatchEvent: ACTION_UP
09-29 00:20:11.155 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: ACTION_UP
09-29 00:20:11.156 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: 我是最后的消费者true
以上就是一个完整的事件派发如图1.
4.如果我们点击后不立马松开并且鼠标移动后再松开
09-29 00:30:45.177 8276-8276/com.example.krcm110.myapplication D/Activity_dispatchEvent: ACTION_DOWN
09-29 00:30:45.177 8276-8276/com.example.krcm110.myapplication D/GroupView_dispatchEvent: ACTION_DOWN
09-29 00:30:45.177 8276-8276/com.example.krcm110.myapplication D/GroupView_onIntercept: ACTION_DOWN
09-29 00:30:45.177 8276-8276/com.example.krcm110.myapplication D/View_dispatchEvent: ACTION_DOWN
09-29 00:30:45.177 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: ACTION_DOWN
09-29 00:30:45.193 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: 我是最后的消费者true09-29 00:30:45.328 8276-8276/com.example.krcm110.myapplication D/Activity_dispatchEvent: ACTION_MOVE
09-29 00:30:45.328 8276-8276/com.example.krcm110.myapplication D/GroupView_dispatchEvent: ACTION_MOVE
09-29 00:30:45.329 8276-8276/com.example.krcm110.myapplication D/GroupView_onIntercept: ACTION_MOVE
09-29 00:30:45.329 8276-8276/com.example.krcm110.myapplication D/View_dispatchEvent: ACTION_MOVE
09-29 00:30:45.329 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: ACTION_MOVE
我是最后的消费者true09-29 00:30:45.572 8276-8276/com.example.krcm110.myapplication D/Activity_dispatchEvent: ACTION_UP
09-29 00:30:45.572 8276-8276/com.example.krcm110.myapplication D/GroupView_dispatchEvent: ACTION_UP
09-29 00:30:45.572 8276-8276/com.example.krcm110.myapplication D/GroupView_onIntercept: ACTION_UP
09-29 00:30:45.572 8276-8276/com.example.krcm110.myapplication D/View_dispatchEvent: ACTION_UP
09-29 00:30:45.572 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: ACTION_UP
09-29 00:30:45.573 8276-8276/com.example.krcm110.myapplication D/View_onTouchEvent: 我是最后的消费者true
总结:
- 事件类型(4种)
事件类型 | 具体动作 |
---|---|
MotionEvent.ACTION_DOWN | 按下View(所有事件的开始) |
MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
MotionEvent.ACTION_MOVE | 滑动View |
MotionEvent.ACTION_CANCEL | 结束事件(非人为原因) |
对你发现了,我们根本没有触发过"ACTION_CANCEL",接下来我们一探究竟,什么时候系统会触发MotionEvent.ACTION_CANCEL,请看下面一句话:
当用户保持按下操作,并从你的控件转移到外层控件时,会触发ACTION_CANCEL
继续添加一个HorizontalScrollView
class MyScrollView(context: Context): HorizontalScrollView(context) { override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { when(ev?.action) { MotionEvent.ACTION_DOWN->{ Log.d("ScrollViw_dispatchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{Log.d("ScrollViw_dispatchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{Log.d("ScrollViw_dispatchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{ (Log.d("ScrollViw_dispatchEvent","ACTION_CANCEL")) } } return super.dispatchTouchEvent(ev) } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { when(ev?.action) { MotionEvent.ACTION_DOWN->{Log.d("ScrollView_onIntercept","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{Log.d("ScrollView_onIntercept","ACTION_MOVE")} MotionEvent.ACTION_UP->{Log.d("ScrollView_onIntercept","ACTION_UP")} MotionEvent.ACTION_CANCEL->{Log.d("ScrollView_onIntercept","ACTION_CANCEL")} } return super.onInterceptTouchEvent(ev) } override fun onTouchEvent(ev: MotionEvent?): Boolean { when(ev?.action) { MotionEvent.ACTION_DOWN->{ Log.d("ScrollView_onTouchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{ Log.d("ScrollView_onTouchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{ Log.d("ScrollView_onTouchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{ Log.d("ScrollView_onTouchEvent","ACTION_CANCEL")} } Log.d("我消费了事件",super.onTouchEvent(ev).toString()); return super.onTouchEvent(ev) } }
然后修改MainActivity
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) var myLinearLayout = MyLinearLayout(this); var myButton = MyButton(this); myButton.setText("MyButton") var arrayList:ArrayList<View> = ArrayList(); arrayList.add(myButton); myButton.width=100; myButton.height=100; var viewGroups:ViewGroup.LayoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT); var viewPar:ViewGroup.LayoutParams = ViewGroup.LayoutParams(1000,400); var viewScroll = MyScrollView(this); viewScroll.addView(myButton); myLinearLayout.addView(viewScroll,viewPar); setContentView(myLinearLayout,viewGroups); } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { when(ev?.action) { MotionEvent.ACTION_DOWN->{ Log.d("Activity_dispatchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{ Log.d("Activity_dispatchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{ Log.d("Activity_dispatchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{ Log.d("Activity_dispatchEvent","ACTION_CANCEL") } } return super.dispatchTouchEvent(ev) } override fun onTouchEvent(event: MotionEvent?): Boolean { when(event?.action) { MotionEvent.ACTION_DOWN->{Log.d("Activity_onTouchEvent","ACTION_DOWN")} MotionEvent.ACTION_MOVE->{Log.d("Activity_onTouchEvent","ACTION_MOVE")} MotionEvent.ACTION_UP->{Log.d("Activity_onTouchEvent","ACTION_UP")} MotionEvent.ACTION_CANCEL->{Log.d("Activity_onTouchEvent","ACTION_CANCEL")} } return super.onTouchEvent(event) } }
运行后当点击Button进行鼠标后进行拖拽出Button然后观察打印
09-29 02:17:39.336 4266-4266/com.example.krcm110.myapplication D/View_dispatchEvent: ACTION_CANCEL
09-29 02:17:39.336 4266-4266/com.example.krcm110.myapplication D/View_onTouchEvent: ACTION_CANCEL
对它触发了,在两个容器穿越的时候他偷偷的触发了。
说到这里我不得不像大家提一嘴如果这个时候不让HorizontalScrollView添加子组件会发生什么?
在MainActivity中屏蔽掉 viewScroll.addView(myButton),然后运行程序后点击鼠标左上角然后观察打印信息。
09-29 02:20:49.935 6003-6003/com.example.krcm110.myapplication D/Activity_dispatchEvent: ACTION_DOWN
09-29 02:20:49.936 6003-6003/com.example.krcm110.myapplication D/GroupView_dispatchEvent: ACTION_DOWN
09-29 02:20:49.936 6003-6003/com.example.krcm110.myapplication D/GroupView_onIntercept: ACTION_DOWN
09-29 02:20:49.937 6003-6003/com.example.krcm110.myapplication D/ScrollViw_dispatchEvent: ACTION_DOWN
09-29 02:20:49.937 6003-6003/com.example.krcm110.myapplication D/ScrollView_onIntercept: ACTION_DOWN
09-29 02:20:49.937 6003-6003/com.example.krcm110.myapplication D/ScrollView_onTouchEvent: ACTION_DOWN
09-29 02:20:49.938 6003-6003/com.example.krcm110.myapplication D/我消费了事件: false
09-29 02:20:49.938 6003-6003/com.example.krcm110.myapplication D/GroupView_onTouchEvent: ACTION_DOWN
09-29 02:20:49.938 6003-6003/com.example.krcm110.myapplication D/Activity_onTouchEvent: ACTION_DOWN
09-29 02:20:50.006 6003-6003/com.example.krcm110.myapplication D/Activity_dispatchEvent: ACTION_UP
09-29 02:20:50.006 6003-6003/com.example.krcm110.myapplication D/Activity_onTouchEvent: ACTION_UP
看见了吧,没有找到消费的View,ViewGroup又依次向上传递回去了,下一次的事件消息就直接在Activity中处理了,系统将
Activity(DispatchTouchEvent)->(VIewGroup)DispatchTouchEvents->(VIewGroup)onInterceptTouchEvent->(VIewGroup)onTouchEvent->(VIewGroup)OnTouchEvent->(Activity)OnTouchEvent
由于没找到消费的View所有的容器的onTouchEvent都返回了Flash,让上家看下货看上家需不需要。当下次事件类型的时候就不会再次传递全部由Activity处理掉。