UI 体系 - View 与自定义 View ★
你的重点短板。 View 体系是应用开发的日常,绘制流程、事件分发是中级面试必考的硬核题。
一、View 绘制三大流程
从 ViewRootImpl.performTraversals() 触发,依次走:
- measure(测量):确定 View 的宽高。
- layout(布局):确定 View 在父容器中的位置。
- 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 顺序
- 绘制背景
drawBackground - 绘制内容
onDraw - 绘制子 View
dispatchDraw - 绘制前景/滚动条
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
三种方式:
- 继承现有 View(如 TextView):扩展功能。
- 继承 View:完全自绘,重写 onMeasure(处理 wrap_content)+ onDraw。
- 继承 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 等应在构造时创建并复用。