Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

UI 体系 - View 与自定义 View ★

你的重点短板。 View 体系是应用开发的日常,绘制流程、事件分发是中级面试必考的硬核题。

一、View 绘制三大流程

ViewRootImpl.performTraversals() 触发,依次走:

  1. measure(测量):确定 View 的宽高。
  2. layout(布局):确定 View 在父容器中的位置。
  3. draw(绘制):把 View 画到画布上。

measure 与 MeasureSpec

MeasureSpec 是 32 位 int:高 2 位是模式,低 30 位是尺寸。三种模式:

  • EXACTLY:精确值(match_parent 或具体 dp)。
  • AT_MOST:最大不超过(wrap_content)。
  • UNSPECIFIED:不限制(ScrollView 子 View)。

父 View 的 MeasureSpec + 子 View 的 LayoutParams 共同决定子 View 的 MeasureSpec。 自定义 View 必须处理 wrap_content:否则 AT_MOST 模式下表现得和 match_parent 一样(因为默认用了父给的最大尺寸),需在 onMeasure 里给 wrap_content 一个默认尺寸。

layout

onLayout 中调用子 View 的 layout(l, t, r, b) 确定位置。View 的 getWidth/getHeight(布局后的实际尺寸)与 getMeasuredWidth/Height(测量尺寸)区别:正常情况相等,但可被 layout 强行改变。

draw 顺序

  1. 绘制背景 drawBackground
  2. 绘制内容 onDraw
  3. 绘制子 View dispatchDraw
  4. 绘制前景/滚动条 onDrawForeground

二、事件分发机制

三个核心方法,贯穿 Activity → ViewGroup → View:

  • dispatchTouchEvent:分发事件,返回 true 表示消费。
  • onInterceptTouchEvent:仅 ViewGroup 有,返回 true 拦截,交给自己的 onTouchEvent。
  • onTouchEvent:处理事件,返回 true 表示消费。

传递规律(U 型):事件从 Activity 向下分发(dispatch),ViewGroup 可在 intercept 拦截;子 View 不消费则向上回传(onTouchEvent 冒泡)。

关键规则:

  • 一旦某 View 在 ACTION_DOWN 返回 true 消费了事件,后续 MOVE/UP 都直接交给它(形成事件序列)。
  • 若 DOWN 没被消费,后续事件不再传给它。
  • requestDisallowInterceptTouchEvent(true):子 View 请求父不要拦截(滑动冲突解决用)。

三、滑动冲突解决

当内外层都能滑动(如 ViewPager 嵌 ListView、横滑嵌竖滑),需解决冲突:

  • 外部拦截法(推荐):重写父容器 onInterceptTouchEvent,按需要判断是否拦截。DOWN 不拦截(否则子 View 收不到),MOVE 时根据方向决定。
  • 内部拦截法:父容器默认拦截所有,子 View 通过 requestDisallowInterceptTouchEvent 动态控制父是否拦截,配合父重写 onInterceptTouchEvent 对 DOWN 不拦截。

判断依据:水平/垂直距离比较、速度、业务规则。

四、自定义 View

三种方式:

  1. 继承现有 View(如 TextView):扩展功能。
  2. 继承 View:完全自绘,重写 onMeasure(处理 wrap_content)+ onDraw。
  3. 继承 ViewGroup:自定义布局,重写 onMeasure(测量子 View)+ onLayout(摆放子 View)。

要点:

  • 自定义属性:attrs.xml 定义 → obtainStyledAttributes 读取。
  • 支持 padding(onDraw 中考虑 paddingLeft 等)。
  • 避免在 onDraw 中 new 对象(每帧调用,造成内存抖动),Paint 等提前创建。
  • 状态保存:重写 onSaveInstanceState/onRestoreInstanceState。

五、invalidate vs requestLayout

  • invalidate():触发重绘(只走 draw),不重新测量布局。UI 内容变了用它。必须在主线程;子线程用 postInvalidate()
  • requestLayout():触发重新 measure + layout(不一定 draw),尺寸/位置变了用它。
  • 硬件加速:GPU 渲染,部分 Canvas API 不支持(老版本),可按 View 关闭。

六、Window / DecorView / ViewRootImpl

  • Window:抽象窗口,PhoneWindow 是唯一实现。
  • DecorView:Window 的顶层 View(含 status bar、content)。
  • ViewRootImpl:连接 WindowManager 和 DecorView,是绘制流程的发起者、事件分发的入口。
  • 关系:Activity → PhoneWindow → DecorView → ViewRootImpl 驱动绘制。
  • 触发时机:View attach 到窗口后,requestLayout/invalidate 等请求会经 ViewRootImpl 调度下一帧 traversal;面试说到这里即可,不要把具体内部调度函数当稳定 API 背诵。

进阶补充:嵌套滑动、帧管线与 RecyclerView

NestedScrolling

嵌套滑动解决父子 View 都想消费滑动的问题。核心是 child 先询问 parent 是否参与,滚动前后分发消耗量。

典型场景:CoordinatorLayout + AppBarLayout + RecyclerView。

Choreographer 与 VSync

Choreographer 接收 VSync 信号,驱动 input、animation、traversal。60Hz 下每帧约 16.6ms,超时会掉帧。卡顿排查要看主线程是否阻塞、布局是否过重、GPU 是否过载。

RecyclerView 复用与预取

  • ViewHolder 复用减少创建成本。
  • DiffUtil 减少无效刷新。
  • GapWorker 负责预取。
  • onBindViewHolder 不做重 IO/复杂计算。

GestureDetector 与 VelocityTracker

复杂手势不要全靠手写坐标判断。点击、长按、fling 可用 GestureDetector;速度计算可用 VelocityTracker。

**追问:**为什么 RecyclerView 滑动卡顿?常见是 bind 太重、图片加载无占位/无取消、布局层级深、频繁全量刷新。


高频面试题

Q1:View 的绘制流程?从哪里开始? 从 ViewRootImpl.performTraversals 开始,依次 performMeasure(measure)→ performLayout(layout)→ performDraw(draw)。measure 确定大小,layout 确定位置,draw 绘制内容。

Q2:MeasureSpec 是什么?三种模式? 32 位 int,高 2 位模式 + 低 30 位尺寸。EXACTLY(精确,match_parent/具体值)、AT_MOST(最大,wrap_content)、UNSPECIFIED(不限,如 ScrollView 子 View)。由父 MeasureSpec + 子 LayoutParams 共同决定。

Q3:自定义 View 直接继承 View,wrap_content 不生效怎么办? 在 onMeasure 中判断模式为 AT_MOST 时,给一个默认尺寸(不能直接用父给的最大值,否则等同 match_parent)。

Q4:事件分发三个方法?返回值含义? dispatchTouchEvent(分发)、onInterceptTouchEvent(ViewGroup 拦截)、onTouchEvent(处理)。返回 true 表示消费,事件序列后续都给它;返回 false 向上回传。

Q5:滑动冲突怎么解决? 外部拦截法(父重写 onInterceptTouchEvent 按需拦截,DOWN 不拦截)或内部拦截法(子用 requestDisallowInterceptTouchEvent 控制)。核心是判断滑动方向/距离。

Q6:invalidate 和 requestLayout 区别?可以在子线程调用吗? invalidate 重绘(走 draw),requestLayout 重新测量布局。invalidate 必须主线程,子线程用 postInvalidate。

Q7:getWidth 和 getMeasuredWidth 区别? getMeasuredWidth 是 measure 后的测量值,getWidth 是 layout 后的实际值(= right - left)。通常相等,但 layout 可强行改变实际尺寸使其不等。

Q8:onDraw 里能不能 new 对象? 不能。onDraw 每帧调用,频繁创建对象导致内存抖动、频繁 GC、卡顿。Paint/Path 等应在构造时创建并复用。