一、基础版回答
核心三阶段:
-
Measure(测量)
-
自顶向下递归测量所有View的宽高
-
关键方法:
onMeasure()
→setMeasuredDimension()
-
注意:
MeasureSpec
的三种模式(EXACTLY/AT_MOST/UNSPECIFIED)
-
-
Layout(布局)
-
根据测量结果确定View的最终位置
-
关键方法:
onLayout()
-
父View通过
layout()
确定子View的四个顶点坐标
-
-
Draw(绘制)
-
将View绘制到屏幕上
-
关键方法:
onDraw()
-
绘制顺序:背景 → 自身内容 → 子View → 装饰(如滚动条)
-
示例回答:
"View绘制就像装修房子:先量尺寸(Measure),再摆家具(Layout),最后粉刷墙面(Draw)。父View负责协调子View的测量和布局,而onDraw()
是每个View自己完成的。"
二、进阶版回答
六个技术要点:
-
硬件加速原理
-
通过
RenderThread
和RenderNode
将绘制指令转为OpenGL/D3D调用 -
使用
DisplayList
记录绘制命令,避免重复执行onDraw
-
-
性能优化关键点
java
复制
// 避免触发不必要的绘制 view.setWillNotDraw(true); // 默认无自定义绘制时设置 canvas.clipRect(); // 限制绘制区域
-
层级优化策略
-
使用
merge
标签减少布局层级 -
通过
ViewStub
延迟加载复杂布局 -
ConstraintLayout
替代多层嵌套
-
-
自定义View实践
kotlin
复制
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // 处理wrap_content默认行为问题 val minWidth = resolveSize(minWidth, widthMeasureSpec) setMeasuredDimension(minWidth, calculateHeight()) }
-
屏幕刷新机制
-
Choreographer
协调VSYNC信号与绘制流程 -
Invalidate()
触发重绘的线程安全机制
-
-
Compose对比
传统View体系 Jetpack Compose 命令式绘制 声明式重组 基于View树遍历 基于状态快照差异 手动优化绘制区域 自动跳过未变化内容
自定义View的注意点
1. 测量阶段注意事项
-
正确处理wrap_content:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 处理wrap_content情况 int width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST ? defaultWidth : MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST ? defaultHeight : MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width, height); }
-
考虑padding:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); // 考虑padding width = getPaddingLeft() + getPaddingRight() + contentWidth; height = getPaddingTop() + getPaddingBottom() + contentHeight; setMeasuredDimension(width, height); }
2. 布局阶段注意事项
-
处理子View的位置(针对ViewGroup):
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); // 计算子View的位置并布局 child.layout(childLeft, childTop, childRight, childBottom); } }
3. 绘制阶段注意事项
-
优化绘制性能:
@Override protected void onDraw(Canvas canvas) { // 避免在onDraw中分配对象,会导致频繁GC // 使用成员变量替代局部变量 paint.setColor(mColor); canvas.drawRect(rect, paint); }
-
正确处理Canvas变换:
@Override protected void onDraw(Canvas canvas) { int saveCount = canvas.save(); // 进行Canvas变换 canvas.rotate(degrees, pivotX, pivotY); // 绘制内容 canvas.drawRect(rect, paint); // 恢复Canvas状态 canvas.restoreToCount(saveCount); }
4. 其他重要注意事项
-
处理触摸事件:
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 处理按下事件 return true; case MotionEvent.ACTION_MOVE: // 处理移动事件 invalidate(); // 请求重绘 return true; case MotionEvent.ACTION_UP: // 处理抬起事件 return true; } return super.onTouchEvent(event); }
-
支持自定义属性:
-
在res/values/attrs.xml中定义属性:
<declare-styleable name="CustomView"> <attr name="customColor" format="color"/> <attr name="customSize" format="dimension"/> </declare-styleable>
-
在构造方法中读取属性:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView); mColor = a.getColor(R.styleable.CustomView_customColor, Color.BLACK); mSize = a.getDimension(R.styleable.CustomView_customSize, 10); a.recycle();
-
-
内存泄漏预防:
-
避免在View中持有Activity的长期引用
-
及时取消Handler、动画等可能造成泄漏的操作
-
-
性能优化:
-
使用
View.isInEditMode()
处理IDE预览时的特殊逻辑 -
对于复杂View,考虑使用
setWillNotDraw(true)
优化 -
合理使用
clipRect()
限制绘制区域自定义View的最佳实践
-
优先考虑组合现有View而非完全自定义
-
合理使用View的缓存机制(如Bitmap缓存)
-
避免过度绘制:使用
canvas.clipRect()
限制绘制区域 -
考虑硬件加速:了解不同API级别硬件加速的限制
-
正确处理生命周期:在适当时机释放资源
-