* 触摸事件派发流程
** 基础认识
*** 1. 事件分发的对象是谁?
1. [ ] 点击 (Touch) 事件
Touch 事件的相关细节(发生触摸的位置,时间等)被封装成 MotionEvent 对象
2. [ ] 事件分发的本质
将点击事件( MotionEvent ) 传递到某个具体的 View &处理的整个过程
事件传递的过程 = 分发的过程
3. [ ] 事件的在哪些对象之间传递?
Activity, ViewGroup, View
4. [ ] 事件的分发顺序
Activity -> ViewGroup -> View
5. [ ] 事件分发过程由哪些方法协作完成?
dispatchTouchEvent(), onInterceptTouchEvent(), onTouchEvent()
dispatchTouchEvent(): 分发(传递)点击事件, 当点击事件能够传递给当前 View 时,
该方法就会被调用
onTouchEvent(): 处理点击事件, 在 dispatchTouchEvent() 内部调用
onInterceptTouchEvent(): 只存在 ViewGroup ,普通的 View 无该方法, 在
ViewGroup 的 dispatchTouchEvent() 内部调用
*** 2. Activity 的事件分发机制
1. [ ]
开始 -> 调用 Activity.dispatchTouchEvent()(一般情况下,点击事件 = Down) ->
getWindow().superDispatchTouchEvent() -> mDecor.supderDispatchTouchEvent()即
ViewGroup.dispatchTouchEvent()实现了事件从 Activity 到 ViewGroup 的传递。->
true/fase
true: Activity.dispatchTouchEvent() 返回 true -> 事件分发结束 -> 结束
false: Activity.dispatchTouchEvent() 返回值 = Activity.onTouchEvent() ->
Activity.onTouchEvent() 无论返回什么,事件分发都结束;在事件在边界范围内时,
默认返回 false -> 事件分发结束 -> 结束
*** 3. ViewGroup 的事件分发机制
1. [ ] ViewGroup 的事件分发机制从 dispatchTouchEvent() 开始
Android 事件分发总是先传递到 ViewGroup, 再传递到 View
开始 -> 调用该控件所在布局 ViewGroup.dispatchTouchEvent() -> 调用
ViewGroup.onInterceptTouchEvent() -> 是否拦截事件
false(不拦截,默认) -> 运行事件继续向子 View 传递 -> 找到被点击的相应的子
View 控件(遍历 ViewGroup 中所有的子 View) -> 调用子 View 控件的
dispatchTouchEvent()(实现了事件从 ViewGroup 到 View 的传递) -> 结束
true(拦截),需要手动复写设置/无 View 接收事件时(点击空白处) -> 不允许事件继
续向子 View 传递 -> 调用 ViewGroup 父类 dispatchTouchEvent()(即
View.dispatchTouchEvent()) -> 自己处理该事件(调用自身的 onTouch() ->
onTouchEvent() -> performClick()- > onClick()) -> 结束
*** 4. View 的事件分发机制
1. [ ] 每当控件被点击时
开始 -> 调用 View.dispatchTouchEvent() -> 调用 View.onTouch() -> onTouch() 的
返回值(手动复写设置)
false(事件无被消费,继续向下传递) -> dispatchTouchEvent() 的返回值 =
onTouchEvent() -> 调用 onTouchEvent() -> 调用 performClick() -> 调用
onClick()(手动回调 setOnClickListener() 为控件注册点击事件) -> 结束
true(事件被消费,不再继续往下传递) -> dispatchTouchEvent() 返回 true (事件不
再往下传递,不调用 onClick()) -> 结束
注: onTouch() 执行先于 onClick()
*** 5. 三者关系& 事件传递规则
1. [ ] 伪代码说明逻辑
步骤1. 调用 dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;//代表是否会消费事件
//步骤2. 判断是否拦截事件
if (onInterceptTouchEvent(ev)) {
//a. 若拦截,则将该事件交给当前 View 进行处理
//即调用 onTouchEvent() 方法去处理点击事件
consume = onTouchEvent(ev);
} else {
//b. 若不拦截,则将该事件传递到下层
//即 下层元素的 dispatchTouchEvent() 就会被调用,重复上述过程
//直到点击事件被最终处理为止
consume = child.dispatchTouchEvent(ev);
}
//步骤3. 最终返回通知 该事件是否被消费(接收 & 处理)
return consume;
}
*** 6. 拦截 Down 事件
1. [ ] onInterceptTouchEvent() 方法一旦返回一次 true,就再也不会被调用
2. [ ] 在 View 接收到 Down 事件之后,若 ViewGroup 拦截了一个半路的事件( 如Move
),该事件将会被系统变成一个 CANCEL 事件&传递给之前处理该事件的 View ; 该事件
不会再传递给 ViewGroup 的 onTouchEvent();只有再到来的事件才会传递到 ViewGroup
的 onTouchEvent()
*** 7. 额外知识
1. [ ] Touch 事件的后续事件(Move ,UP)层级传递
1. 若给控件注册了 Touch 事件,每次点击都会触发一系列 action 事件 (
ACTION_DOWN, ACTION_MOVE, ACTION_UP 等)
2. 当 dispatchTouchEvent() 事件分发时,只有前一个事件(如 ACTION_DOWN) 返回
true, 才会收到后一个事件(ACTION_MOVE 和 ACTION_UP); 即如果在执行
ACTION_DOWN 时返回 false,后面一系列的 ACTION_MOVE, ACTION_UP 事件都不会执
行
3. 从上面对事件的分发机制分析知:
1. dispatchTouchEvent(), onTouchEvent() 消费事件,终结事件传递(返回 true)
2. 而 onInterceptTouchEvent 并不能消费事件,它相当于一个分叉路口起到分流导
流的作用,对后续的 ACTION_MOVE 和 ACTION_UP 事件接收起到非常大的作用
3. 接收了 ACTION_DOWN 事件的函数不一定能接收到后续事件(ACTION_MOVE,
ACTION_UP)
1. [ ] 关于 ACTION_MVOE 和 ACTION_UP 事件的传递结论
1. 若对象(Activity, ViewGroup, View) 的 dispatchTouchEvent() 分发事件消费了
事件(返回 true),那么收到 ACTION_DOWN 的函数也能收到 ACTION_MOVE 和
ACTION_UP
2. 若对象(Activity, ViewGroup, View)的 onTouchEvent() 处理了事件(返回
true),那么 ACTION_MOVE, ACTION_UP 的事件从上往下传到该 View 后就不再往下
传递,而是直接传递给自己的 onTouchEvent()& 结束本次事件的传递过程
1. [ ] onTouch 和 onTouchEvent() 的区别
1. 该 2 个方法都是在 View.dispatchTouchEvent() 中调用
2. 但 onTouch 优先于 onTouchEvent() 执行,若手动复写 onTouch() 中返回
true(即将事件消费掉),将不会再执行 onTouchEvent()
* View 绘制流程
** 自定义 View Measure 过程
*** 三种测量模式
1. [ ] UNSECFIED : 父视图不约束子视图 View (即 View 可取任意尺寸);系统内部
(ListView, ScrollView);一般自定义 View 用不到
2. [ ] EXACTLY : 父视图为子视图设定一个确切的尺寸;子视图大小必须在该指定尺寸内;
强制性是子视图的大小扩展至与父视图大小相等(match_parent); 具体数值(如
100dp 或 100px)
View 的最终大小即 Spec 的值
父控件可通过 MeasureSpec.getSize() 直接得到子控件的尺寸
3. [ ] AT_MOST: 父视图为子视图指定一个最大尺寸,子视图必须确保自身 & 所有子视图
可适应在该尺寸内
自适应大小(wrap_content)
将大小设置为包裹我们的 View 内容,那么尺寸大小即为父 View 给我们作为参考的尺
寸;只要不超过该尺寸即可,具体尺寸根据需求设定
该模式下,父控件无法确定子 View 的尺寸,只能由子控件自身更加需求计算尺寸
该模式 = 自定义视图需实现测量逻辑的情况
*** MeasureSpec 值的计算
1. [ ] 子 View 的 MeasureSpec 值根据子 View 的布局参数 (LayoutParams) 和 父容器
的 MeasureSpec 值计算得来的,具体计算逻辑封装在 getChildMesasureSpec()
| 父视图测量模式(mode) | | | |
| 子视图布局参数(LayoutParams) | EXACTLY | AT_MOST | UNSPECIFIED |
| | | | |
|--------------------------------+--------------------------------+--------------------------------+---------------------|
| 具体数值(dp/px) | | | |
| | EXACTLY + childSize | EXACTLY + childSize | EXACTLY + childSize |
|--------------------------------+--------------------------------+--------------------------------+---------------------|
| match_parent | EXACTLY + parentSize | AT_MOST + parentSize | UNSPECIFIED + 0 |
| | (父容量的剩余控件) | (大小不超过父容量的剩余空间) | |
|--------------------------------+--------------------------------+--------------------------------+---------------------|
| wrap_content | AT_MOST + parentSize | AT_MOST + parentSize | UNSPECIFIED + 0 |
| | (大小不超过父容量的剩余空间) | (大小不超过父容量的剩余空间) | |
|--------------------------------+--------------------------------+--------------------------------+---------------------|
区别于顶级 View (即 DecoreView)的测量规格 MeasureSpec 计算逻辑,取决于自身布局
参数 & 窗口尺寸
2. [ ] measure 过程详解
| View 类型 | measure 过程 |
|-----------+----------------------------------------|
| 单一 View | 只测量自身一个 View |
|-----------+----------------------------------------|
| ViewGroup | 对 VewGroup 中所有的子 View 都进行测量 |
| | (即遍历调用所有子元素的 measure() & 各子元素再递归执行该流程) |
1. [ ] measure 绘制流程
单一 View : 开始测量 -> measure() -> onMearuse() -> setMeasureDimension() ->
getDefaultSize() -> 完成测量
measure() 方法 final 修饰,子类不能重写
protected int getSuggestedMiniumWidth() {
return (mBackGround == null) ? mMinWidth : max(mMinWidth, mBackGround.getMinimumWidth())
}
public int getMinimumWdith() {
final int intrinsicWidth = getIntrinsicWidth()
//使用背景图 Drawable 的原始宽度
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
| 方法 | 作用 | 备注 |
|-----------------------+---------------------------------------------------------------------+-------------------------------------------------------|
| | | |
| measure() | 基本测量逻辑的判断 | 属于 View.java 类 & final 类型 |
| | 调用 onMeasure() 进行下 1步的测量 | 即子类不能重写此方法 |
|-----------------------+---------------------------------------------------------------------+-------------------------------------------------------|
| onMeasure() | 1. 根据 View 宽/高 的测量规格计算 View 的宽/高值: getDefaultSize() | |
| | 2. 存储测量后的子 View 宽/高; setMeasureDimension() | |
|-----------------------+---------------------------------------------------------------------+-------------------------------------------------------|
| getDefaultSize() | 根据 View 宽/高的测量规格计算 View 的宽/高值 | 模式为 UNSPECIFIED 时,使用提供的默认大 |
| | | 模式为 AT_MOST, EXACTLY 时,使用 View 测量后的宽/高值 |
|-----------------------+---------------------------------------------------------------------+-------------------------------------------------------|
| setMeasureDimension() | 存储测量后的子 View 宽/高 | |
|-----------------------+---------------------------------------------------------------------+-------------------------------------------------------|
ViewGroup 的 Measure 过程
开始测量 -> measure() -> onMeasure()(需重写) -> measureChildren() ->
measureChild() -> getChildMesasureSpec() -> 遍历子 View 测量/合并 ->
setMeasureDimension() -> 完成测量
为什么 ViewGroup 的 measure 过程不像单一 View 的 measure 过程那样对
onMeasure() 做统一的实现?
答:因为不同的 ViewGroup 子类(LinearLayout, RelativeLayout/自定义 ViewGroup
子类等)具备不同的布局特性,这导致他们子 View 的测量方法各有不同
而 onMeasure() 的作用 = 测量 View 的宽/高
因此, ViewGroup 无法对 onMeasure() 作统一实现。这个也是单一 View 的 measure
过程与 ViewGroup 过程最大的不同
1. 即单一 View measure 过程的 onMeasure() 具有统一实现,而 ViewGroup 则没有
2. 注:其实,在单一 View measure 过程中,getDefaultSize 只是简单的测量了宽高
值,在实际使用时有时需要更精细的测量。所以有时候也需要重写 onMeasure()
| 方法 | 作用 | 备注 |
|------------------------+------------------------------------------------------------+-------------------------------------------|
| | | |
| measure() | 基本测量逻辑的判断 | 类似单一 View measure 过程 |
| | 调用 onMearuse()进行下1步测量 | |
|------------------------+------------------------------------------------------------+-------------------------------------------|
| onMeasure() | 1. 遍历所有子 View & 测量: measureChildren() | 因不同的 ViewGroup 子类具备不同的布局特性 |
| | 2. 合并所有子 View 尺寸,计算出最终 ViewGroup 的尺寸 | 这导致他们子 View 的测量方法各有不同, |
| | (根据布局特性实现) | 故此方法需重写 |
| | 3. 存储测量后的子 View 宽/高: setMeasureDiemension() | |
|------------------------+------------------------------------------------------------+-------------------------------------------|
| | | |
| measureChildren() | 遍历子 View | |
| | 调用 measureChild() 进行子 View 下一步测量 | |
| | | |
|------------------------+------------------------------------------------------------+-------------------------------------------|
| | | |
| measureChild() | 计算单个子 View 的 MeasureSpec; getChildMesasureSpec() | 后续即进入单一 View 的 measure 过程 |
| | 调用每个字 View 的 measure() 进行下一步的测量 | |
|------------------------+------------------------------------------------------------+-------------------------------------------|
| getChildMesasureSpec() | 计算子 View 的 Measurespec 参数 | |
| | (计算因素: 父 View 的 measureSpec 和 子 View 的布局参数) | |
|------------------------+------------------------------------------------------------+-------------------------------------------|
| | 存储测量后的子 View 宽/高 | |
| setMeasureDiemension | | |
| | | |
|------------------------+------------------------------------------------------------+-------------------------------------------|
** 自定义 View Layout 过程
*** 1. layout 过程详解
| View 类型 | layout 过程 |
|-----------+---------------------------------------------------------------------------------|
| 单一 View | 仅计算本身 View 的位置 |
| | |
|-----------+---------------------------------------------------------------------------------|
| | 除了计算自身 View 的位置外,还需要确定子 View 在父容器中的位置 |
| ViewGroup | 即:遍历调用所有子元素的 measure() & 各子元素再递归执行该流程 |
| | View 树的位置是由包含的每个子视图的位置 决定 |
| | 故若想计算整个 View 的位置,则需要递归计算每个子视图的位置(类似 measure 过程) |
|-----------+---------------------------------------------------------------------------------|
1. [ ] 单一 View 的 layout 过程
开始计算位置 -> layout() -> onLayout() -> 完成计算
laoyout(): 计算自身 View 的位置(调用 setFrame()/ setOpticalFrame())
onLayout(): 空实现
2. [ ] ViewGroup 的 layout 过程
开始计算位置 -> layout() -> onLayout() -> (子 View1 -> layout() ->
onLayout()) -> 完成计算
ViewGroup.layout(): 完成父 View 的位置绘制(调用 setFrame())
ViewGroup.onLayout(): 确定子 View 在父容器的位置()(需复写 & 调用子 View 的
layout())
View.layout(): 计算自身 View 的位置 (调用 setFrame())
View.onLayout(): 空实现
| 类型 | 作用 | 赋值时机 | 赋值方法 | 值大小 | 使用场景 |
|---------------------+-----------------------+--------------+------------------------------------+---------------------------------+------------------------------------------------|
| getMeasuredWidth() | 获取 View 测量的宽/高 | measure 过程 | setMeasureDimension() | 一般情况下,二者获取的宽/高相等 | |
| getMeasuredHeight() | | | | | 在 onLayout() 中使用 getMeasureWidth 获取宽/高 |
|---------------------+-----------------------+--------------+------------------------------------+---------------------------------+------------------------------------------------|
| getWidth() | | | | 一般情况下,二者获取的宽/高相等 | |
| getHeight() | 获取 View 最终的宽/高 | layout 过程 | layout()中传递四个参数之间的运算 | | 在 onLayout() 外的地方用 getWidth() 获取宽/高 |
|---------------------+-----------------------+--------------+------------------------------------+---------------------------------+------------------------------------------------|
上面标注:一般情况下,二者获取的宽/高是相等的,那么 “非一般” 情况时什么?
答:人为设置:通过重写 View 的 layout() 强行设置
@override
public void layout(int l, int t, int r, int b) {
//改变传入的顶点位置参数
super.layout(l, t, r+100, b+100)
//如此一来,在任何情况下 getWidth()/getHeight() 获取到的宽/高 总比
getMeasuredWidth()/getMeasuredHeight() 获取的宽/高大 100px
即: View 的最终宽/高,总比测量宽/高大 100px
}
| View 类型 | layout过程 |
|-----------+------------------------------------------------------------------------|
| 单一 View | 仅计算本身 View 的值 |
|-----------+------------------------------------------------------------------------|
| ViewGroup | 1. 计算自身的位置:layout() |
| | 2. 遍历子 View,计算子 View 的位置 & 设置 |
| | (复写)ViewGroup.onLayout, 子 View.layout(), 子 View.onLayout() |
| | 3. 如此不断循环,最终确定所有子 View 在父容器的位置,即 layout过程完毕 |
| | |
|-----------+------------------------------------------------------------------------|
** 自定义 View Draw 过程
*** 1. draw 过程详解
| View类型 | Draw 过程 |
|-------------+----------------------------------------------|
| 单一 View | 仅绘制视图 View 本身 |
| | |
|-------------+----------------------------------------------|
| ViewGroup | 除了绘制自身 View,还需要绘制其他所有子 View |
| (含子 View) | |
|-------------+----------------------------------------------|
1. [ ] 单一 View 的 draw 过程
开始绘制 -> draw() -> drawBackgroud() -> onDraw() -> dispatchDraw() ->
onDrawScrollBars() -> 结束绘制
draw(): 绘制本身
drawBackGround():绘制自身 View 的背景
onDraw():绘制自身 View 的内容
dispatchDraw(): 空实现(因为没有子 View)
onDrawScrollBars():绘制装饰
2. [ ] ViewGroup 的 draw 过程
开始绘制 -> draw() -> drawBackground() -> onDraw() -> dispatchDraw()
ViewGroup部分
遍历子 View
draw() -> drawBackground() -> onDraw() -> dispatchDraw() -> onDrawScrollBars()
->
onDrawScrollBars() -> 完成绘制
ViewGroup.draw():绘制自身
ViewGroup.drawBackGround(): 绘制自身背景
ViewGroup.onDraw(): 绘制自身内容(需要重写)
ViewGroup.dispatchDraw(): 绘制子 View (不需要重写)
子View.draw(): 绘制自身
子view.drawBackgroud(): 绘制自身 View 的背景
子View.onDraw(): 绘制自身 View 的内容(需要重写)
子view.dispatchDraw(): 空实现
子view.onDrawScrollBars(): 绘制装饰
3. [ ] View.setWillNotDraw()
a. 该标记的作用是:当一个 View 不需要绘制内容时,系统进行相应的优化
b. 默认情况下:View 不启用该标记位(设置为 false); ViewGroup 默认启用(设置
为 true)
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK)
}
应用场景
a. setWillNotDraw 参数设置为 true: 当自定义 View 继承自 ViewGroup,且本身并不
具备任何绘制时,设置为 true 后,系统进行相应的优化
b. setWillNotDraw 参数设置为 false; 当自定义 View 继承自 ViewGroup,且需要绘制
内容时,那么设置为 false,来关闭 WILL_NOT_DRAW 这个标记位