View 的事件体系
View基础知识
一、View
的相关坐标和位置
先来了解一个概念:
view动画
不改变view
的真实位置,就是肉眼看上去,view
位置发生了变化,但是它的点击区域还是在原来的位置。
为了方便描述,下文中的的 真实View
,表示真实位置的View
,看到的View
,表示肉眼看到的View
。
属性动画和 setTranslationX
/ setTranslationY
,会改变显示位置和真实位置。
上图中view1
是初始位置,通过执行setTranslation
函数,得到的view2
位置,其中left
,top
,right
,bottom
这几个值不会改变。
left
,top
,right
,bottom
View
的位置主要由它的四个顶点的位置来决定,分别对应 View 的四个属性:left
,top
,right
,bottom
。对应的函数是getLeft
,getTop
,getRight
,getBottom
。
属性 | 含义 |
---|---|
left |
View 左上顶点相对于父容器的横坐标 |
top |
View 左上顶点相对于父容器的纵坐标 |
right |
View 右下顶点相对于父容器的横坐标 |
bottom |
View 右下顶点相对于父容器的纵坐标 |
这几个值表示的是View的真身位置,它表示哪里,点击区域就在哪里。和肉眼在屏幕上看到的位置可能不一样。
X
,Y
上面说到view动画不改变view
的真实位置(也就是不改变view
的left
,top
,right
,bottom
)。
看到的view相对于真实view
,位置发生了变化,但是它的点击区域还是在真实view的位置。
从 Android 3.0 开始,View
增加了 x,y,translationX
和 translationY
。
x,y 是看到的view
的左上角相对父容器的坐标,但不同于 left 和 top ,这两个坐标点的值并一定都是相等的。
真实view
和 看到view
的偏差用 translationX
和 translationY
来表示
。
translationX
,translationY
android开发艺术探索中,关于translationX
的讲解是错误的。真实情况是,translationX
是真实view
相对于看到view
的 x 方向偏移量。
从上图可以得知:x
= left
+ translationX
。
二、点击事件的XY
坐标
三、MotionEvent
常见的触摸动作一共有三个:
ACTION_DOWN
手指按下动作ACTION_MOVE
手指滑动动作ACTION_UP
手指抬起动作
四、GestureDetector
手势检测,检测用户的单击、双击、长按等操作。
通过创建GestureDetector
对象并实现OnGestureListener
接口(检测单击相关手势)或者OnDoubleTapListener
接口(检测双击相关动作)。
五、TouchSlop
不同安卓设备认为的最小的滑动距离,低于此距离,将不认为用户在滑动,开发者可自行更改,默认为8dp
。
六、VelocityTracker
速度检测,可以计算出用户的手势的滑动速度。
注意,在VelocityTracker#computeCurrentVelocity(int time)
中的time
参数是我们计算速度的单位时间,相同的速度用不同的单位时间会有不同的表示方法。
七、Scroller
弹性滑动对象,由于View中的scrollTo/scrollBy
都是瞬间完成位移的。对用户的使用体验不太友好,所以引入弹性滑动对象,使得View
的滑动变得可视化。
View的滑动
一、scrollTo
/scrollBy
View有专门的方法实现滑动,即scrollTo()
, scrollBy()
;
这两个函数只能将View
中间的内容进行位移,但是不能改变View
本身的位置。
可以方便的实现滑动效果并且不影响内部元素的点击事件。
scrollTo(int destX, int destY)
直接让View出现在目标位置,无中间动画。
参数变量的单位都是像素级。
二、使用动画
有两种动画的使用方法。
View
动画在
XML
文件里面通过改变translationX
/translationY
的方式来实现View的移动效果。属性动画
在
Android3.0
版本以上,可以使用ObjectAnimator
类来实现属性动画的效果。在
Android3.0
以下的版本中,我们需要自己加载开源动画库NineOldAndroids
。
在XML
文件中,有一个andoird:fillAfter="true|false"
的选项。
当为true
的时候,View在动画结束后,图像将停留在目标位置上。
当为false
的时候,View会在动画结束后,从末位置消失,重新在初位置出现。
不论是true
还是false
,View的点击事件的坐标还是在原先的初始位置上,并不会随着View
的移动而移动。
在Android3.0
以上使用属性动画可以解决这样的问题。
三、改变布局参数
通过修改View
的marginLeft
等等布局参数,以此来达到View
滑动的效果。
弹性滑动
一、Scroller
Scroller#smoothScrollTo(int destX, int destY)
将目标位置与初始位置的偏移值delta计算后调用startScroller()
方法存储相关参数,并开始使用invalidate()
方法重绘View
。重绘View
时的draw()
方法会调用computeScroll()
方法,其中又会调用invalidate
方法重绘View
,不断的循环。由于scrollTo(int destX, int destY)
直接让View出现在目标位置,所以我们在computeScroll()
方法中采取微分法的做法,即将一段长距离拆分成许多微小的距离。不断通过scrollTo()
的直接滑动以及computeScrollOffset()
的重新计算下一个短距离的相关参数以及postInvalidate()
的重绘View,最终达到弹性滑动的效果。
computeScrollOffset()
方法通过计算目前已经位移的百分比来计算下一次的位移目标以及是否已经完成的滑动,不通过计时器等工具。
flowchart TD
A[startScroll]
B[invalidate]
C[computeScroll]
D[computeScrollOffset : boolean]
E[invalidate]
A --> B -->|draw| C --> D -->|not finish| E -->|draw| C
D -->|finish| F[finish]
二、通过动画
通过ObjectAnimator类
我们可以直接的通过动画完成View
的弹性滑动。
我们也可以通过onAnimatorUpdate
方法还自定义自己想要的动画效果。
三、使用延时策略
可以通过Thread#sleep
或者Handler#postDelayed
方法来达到间隔一定的时间就改变一次微小位置变化的操作。
在此次的scrollTo
方法执行结束后,通过sleep|postDelayed
方法暂停一定的时间,然后重复执行scrollTo
的方法,以此来达到弹性滑动的效果。
注意:无法在精准的时间内移动固定的位移,因为系统的消息调度(sleep|postDelayed
)也是需要时间的。
View
的事件分发机制
一、点击事件的传递规则
点击事件分发过程,即将一系列的MotionEvent
事件序列(由ACTION_DOWN
开始到ACTION_UP
结束的一连串操作)交给一个View
执行的过程。
点击事件的分发主要由三个重要方法构成:dispatchTouchEvent
, onInterceptTouchEvent
,onTouchEvent
。
三个方法的执行顺序如下:
1 |
|
其中执行事件序列的不同方法中同样由优先顺序:onTouchListener > onTouchEvent > onClickListener
当点击事件发生后,它的传递过程会遵循以下的顺序:Activity -> Window -> DecorView -> View -> ...
、
同时,如果子级发现无法执行这个事件的时候,那么它的父容器的onTouchEvent
就会重新调用,直到Activity
。
其中,ViewGroup
默认不拦截任何事件,View
没有onInterceptTouchEvent
方法。
二、事件分发解析
Activity
对点击事件的分发
1 |
|
Window
对点击事件的分发
1 |
|
顶级View
对点击事件的分发
由此开始,将执行(一)所讲述的事件传递规则。其主要的部分是ViewGroup
的事件传递。顶级View
一般来说都是ViewGroup
。
判断当前ViewGroup
是否拦截点击事件
1 |
|
代码中的几个变量的作用:
当事件由
ViewGroup
的子元素处理成功时,mFirstTouchTarget
就会被赋值并指向子元素,此时的mFirstTouchTarget != NULL
。FLAG_DISALLOW_INTERCEPT
一旦被设置后,那么当前的ViewGroup
就无法拦截ACTION_MOVE
以及ACTION_UP
。由于事件为
ACTION_DOWN
的时候,ViewGroup
会重置FLAG_DISALLOW_INTERCEPT
,所以每次事件为ACTION_DOWN
的时候,都会执行一次onInterceptTouchEvent
方法。
第3行的判断语句的意思为:
- 如果当前的事件为
ACTION_DOWN
时,作为一个点击事件的开始,需要向子元素传递,返回true
。允许向子元素传递。 - 如果当前的事件为
ACTION_MOVE
或者ACTION_UP
的时候,如果ACTION_DOWN
已经被子元素处理了(mFirstTouchTarget != NULL
),那么由于一系列的点击事件都要有同一个View
处理,则不能在此拦截,故返回true
。反之,如果mFirstTouchTarget == NULL
,则代表ACTION_DOWN
是由ViewGroup
自身处理的,则不能向下传递,返回false
。
ViewGroup
在ACTION_DOWN
到来时的重置操作
1 |
|
在resetTouchState
方法中会对FLAG_DISALLOW_INTERCEPT
进行重置。
因此子View
的requestDisallowInterceptTouchEvent
方法并不能影响ViewGroup
对ACTION_DOWN
事件的处理。
ViewGroup
不拦截事件时,对点击事件的分发
1 |
|
不断的遍历当前ViewGroup
的所有子元素,如果子元素不在播放动画以及点击事件的坐标落在当前子元素的区域内,那么这个子元素就是一个可以传递的。
由
TouchTarget
源码可知:
TouchTarget
保存了响应触摸事件的子view
和该子view上的触摸点ID集合,表示一个触摸事件派发目标。通过next
成员可以看出,它支持作为一个链表节点储存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final class TouchTarget {
// ···
// The touched child view.
// 被触摸的子元素
@UnsupportedAppUsage
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
// 指针 id 的位的掩码组合,用于目标捕获的所有指针
public int pointerIdBits;
// The next target in the target list.
// 目标列表中的下一个目标
public TouchTarget next;
// ···
}
————————————————
版权声明:本文为CSDN博主「分则能成」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dehang0/article/details/104317611
如果是第一个可传递元素,那么就会进入初始化部分,其中的dispatchTransformedTouchEvent
方法实际上就是调用的子元素的dispatchTouchEvent
方法。在该方法中有如下的一段内容:
1 |
|
由于前面的代码中dispatchTransformedTouchEvent
方法的第三个参数为child
,所以会执行子元素的dispatchTouchEvent
方法,点击事件交由子元素处理,从而完成了一轮事件的分发。
当子元素的dispatchTouchEvent
方法返回true
时,dispatchTransformedTouchEvent
方法同样也会返回true
,接着便执行if语句
的代码块部分了。如果dispatchTouchEvent
方法返回false
的话,ViewGroup
就会把事件向后遍历,寻找新的可传递的子元素。在if语句
的代码块中,mFirstTouchTarget
会被赋值同时跳出for循环
。
其中mFirstTouchTarget
的赋值由addTouchTarget
方法完成,代码如下:
1 |
|
可以很容易的看出,mFirstTouchTarget
其实是一个单链表结构,mFirstTouchTarget
是否被赋值,将直接影响ViewGroup
对事件的拦截策略。
ViewGroup
中没有合适的子元素
有两种情况,ViewGroup
中会没有合适的子元素可以传递:
ViewGroup
没有子元素- 子元素处理了点击事件,但是
dispatchTouchEvent
方法返回了false
,这一般是因为子元素在onTouchEvent
中返回了false
。
在这两种情况中,ViewGroup
都会自己处理点击事件。代码如下:
1 |
|
可以看到,代码中的dispatchTransformedTouchEvent
方法的第三个参数将其设置为null
,此时它就会调用super.dispatchTouchTarget
方法了。
View
对点击事件的处理过程
这里的View
不包含ViewGroup
,因为没有子元素,所以不用向下分发事件,只能自己处理事件。
先看它的dispatchTouchEvent
方法:
1 |
|
由上面的代码可以看出,View
对点击事件的处理流程中,首先会判断有没有设置onTouchListener
,如果有且其中的onTouch
方法返回true
,则onTouchEvent
方法就不会被调用。
从其他的代码中也可以得出的一些结论:
- 如果
View
的CLICKABLE
以及LONG_CLICKABLE
中有一个为true
,不论View
是否为DISABLE
,都会消耗事件(也就是即使View
看起来没有任何的反应,但是也消耗了点击事件)。 View
的LONG_CLICKABLE
默认为false
,而CLICKABLE
是否为false
与其View
有关,如button
的默认为true
、textview
的默认为false
。- 当
ACTION_UP
事件发生时,会触发performClick
方法,如果View
设置了OnClickListener
,那么performClick
方法就会调用它的onClick
方法。(即onClick
的触发前提时View
是可点击的,且收到了DOWN
和UP
的点击事件。) setOnClickListener
方法会自动将CLICKABLE
设置为true
,setOnLongClickListener
方法会自动将LONG_CLICKABLE
设置为true
。
View
的滑动冲突
总共会出现三种滑动冲突的情况:
内部View
与外部View
的滑动方向相反。内部View
与外部View
的滑动方向相同。- 前两种情况的嵌套。
三种情况的处理思路:
- 通过手势滑动的角度判断滑动的方向。
- 通过当前处于的不同的页面状态来判断应该滑动的
View
。 - 通过前两种的综合使用。
滑动冲突的解决方式
外部拦截法
通过重写父容器的onInterceptTouchEvent
方法,所有的事件都先经过父容器的筛选,对其中父容器需要的事件进行拦截。
1 |
|
内部拦截法
父元素拦截除ACTION_DOWN
以外的其他事件,当事件到达子元素后,由子元素判断是否需要这些事件,不需要的事件将重新交由父容器来处理。这种方法和Android的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent
方法才能正常的工作。
相关阅读链接:Android TouchEvent之requestDisallowInterceptTouchEvent - 简书 (jianshu.com)
子元素的dispatchTouchEvent
方法
1 |
|
父容器的onInterceptTouchEvent
方法:
1 |
|