Android View 绘制流程和触摸事件分发流程总结

* 触摸事件派发流程
** 基础认识
*** 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 这个标记位
  
   
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值