关于事件传递的流程,已经有很多大神介绍过了,我在使用的过程中,也遇到了一些问题,在此整理一下,相信有不少同学也有遇到我这样的问题。
问题一:为什么我的onTouchEvent
方法只响应了MotionEvent.ACTION_DOWN
动作
百度或者Google一搜有一大把这样问题。其根本原因是你的MotionEvent.ACTION_DOWN
行为没有被消费掉(没有return true
),onTouchEvent
默认MotionEvent.ACTION_DOWN
为一个触摸事件的开始,如果MotionEvent.ACTION_DOWN
没有做处理,则后面一系列行为将都不被响应。
举一个例子,有一个嵌套布局
- A : ViewGroup
- B : ViewGroup
- C : View
注意,只有
ViewGroup
有onInterceptTouchEvent
方法,View是没有onInterceptTouchEvent
方法的,因为View
不能再有子View,不涉及到事件传递
当我们手指触摸C控件的时候,事件的传递过程为
- A - dispatchTouchEvent
- A - onInterceptTouchEvent
- B - dispatchTouchEvent
- B - onInterceptTouchEvent
- C - dispatchTouchEvent
- C - onTouchEvent
- B - onTouchEvent
- A - onTouchEvent
以上为 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 均未重写的情况
默认情况下,你的ACTION_DOWN
是没有被消费掉的,super.onTouchEvent(event)
会返回false
,比较常见的,有不少人会问,我手指触摸了C控件,但是为什么ACTION_MOVE
和ACTION_UP
没有响应呢,如果想要监听ACTION_MOVE
和ACTION_UP
,我们只需要重写C
的onTouchEvent
方法,在接收到MotionEvent.ACTION_DOWN
的时候返回true
,就代表这个触摸事件交给C
处理了。
那么事件传递的过程将变成:
- A - dispatchTouchEvent
- A - onInterceptTouchEvent
- B - dispatchTouchEvent
- B - onInterceptTouchEvent
- C - dispatchTouchEvent
- C - onTouchEvent
就没有B、A 的onTouchEvent
方法什么事了
问题二:手指在C控件上按下,我想要横着滑动的时候交给控件C处理,竖着滑动的时候交给B处理该如何实现
这里就涉及到事件的拦截处理,就轮到onInterceptTouchEvent
方法大显身手的时候了。
首先聊一聊思路,我们都知道,事件的分发是由外到内的,即当我们在C
控件上按下的时候,事件是先传到A
上,A
传给B
,B
再传给C
(A -> B -> C)。事件处理是由内到外的,即C
先处理,处理完再交给B
,B
处理完再交给A
(C -> B -> A)。当前的需求涉及到B和C来处理滑动状态,那么,我们就可以在滑动事件分发到B
的时候判断一下,当前的滑动是横向滑动的还是纵向滑动的:
- 如果是横向滑动的,那么
onInterceptTouchEvent
方法不做拦截,允许事件传到C
,C
接收到事件后,在onTouchEvent
中将事件消费掉,这样B
就不需要进行处理(不会接收到onTouchEvent的响应)。 - 如果是纵向滑动的,那么
onInterceptTouchEvent
就可以返回true
,将事件拦截下来(不需要交给C处理,C就不会收的onTouchEvent的响应),B
自己在onTouchEvent
中将事件消费掉。
举个栗子
现在我们想实现这样一个功能,在页面上下滑动可以翻页,在控件上左右滑动可以拦截翻页动作
思路:
首先要写布局,自定义个 View 继承 ViewGroup
, 然后开始我们的xml布局
|
|
写完布局我们要重写onMeasure
和onLayout
方法,开始对布局测量、排布。
我们这里是纵向排布。
|
|
写到这里运行程序,应该是可以看得到第一个页面了,但是滑动没有任何反应,我们需要重写onTouchEvent
方法,让View可以根据手指的滑动而滚动,这里需要监听event的ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
、ACTION_CANCEL
状态,通过ACTION_MOVE
记录手指每次纵向移动的距离,在监听到ACTION_UP
、ACTION_CANCEL
的时候,判断滑动位置,然后滚动到哪个页面。这样,一个简单的页面滑动就实现了。
scrollBy、scrollTo
提到滚动,就不得不提到两个方法,scrollBy
和scrollTo
,值得注意的是,通常我们一种面向对象的思想,想让谁滚动,谁就调用自己的scroll就行了,但是,这里的scrollBy
和scrollTo
针对的是自己子View的运动,说白话了,就是老爸要管理儿子运动的。scrollBy
是控制一次滚动多少,scrollTo
是控制滚动到哪里。
我们这里自定义了ScrollViewPager,页面内容都写在ScrollViewPager内部,所以,我们控制滚动,就是在控制ScrollViewPager内部的子View滚动,所以,直接调用scrollBy
和scrollTo
即可。
Scroller
现在滑动没有问题了,但是手指一松开,View 会瞬间移动到指定位置,体验不太好,我们想要View的滚动有一个顺滑的过程,这就又不得不再提到一个Scroller
类,他可以帮助我们完成动画的平缓过度。
通过Scroller
的startScroll
来初始化运动的起始位置、结束位置和运动时间,然后调用invalidate()
开始处理,这里会一直回调View
的computeScroll
方法,是一个空方法,需要我们自己在这里自己依据Scroller
的computeScrollOffset()
判断移动是否完成,然后调用scrollTo
来完成平滑移动处理。
到这里,View就可以实现上下滑动,并平滑过渡了,接下来来处理事件的分发问题。
我们想要实现的目标是,当我们上下滑动的时候,可以滚动页面,要将事件拦截。
当我们在子控件上左右滑动的时候,可以将事件先交给子控件处理。那么我们就可以重写ScrollViewPager
的onInterceptTouchEvent
方法,在ACTION_MOVE
的时候判断滑动方向,如果是上下滑动,则返回true,将事件拦截即可。
下面是自定义ScrollViewPager
源码(Kotlin代码):
|
|