在Android中,View是构成用户界面的基本元素之一。了解View的绘制流程对于开发人员来说非常重要,可以帮助他们更好地理解和优化应用程序的性能。
View的绘制过程可以总结为以下几个步骤:
测量(Measure):在绘制之前,View首先要进行测量,以确定它的大小。这一步是通过调用measure()
方法实现的。在测量过程中,View会根据它的布局参数和父容器的约束条件,计算出自己的测量宽度和高度。
布局(Layout):在测量完成后,View进入布局阶段。在这个阶段,View会调用layout()
方法来确定自己在父容器中的位置。布局过程中,View会根据父容器的布局参数和自身的测量结果,计算出自己的位置和大小。
绘制(Draw):在布局完成后,View进入绘制阶段。在这个阶段,View会调用draw()
方法来绘制自己的内容。绘制过程中,View会根据自己的测量结果和布局结果,将自己的内容绘制到屏幕上。
为了提高应用程序的性能,我们可以采取一些优化措施来减少View的绘制次数和绘制时间:
避免过多的嵌套布局,因为嵌套布局会增加测量和布局的时间消耗。
使用ViewStub
来延迟加载复杂的布局,只有在需要显示时才真正进行测量、布局和绘制。
使用ViewGroup.setClipChildren(false)
来关闭子View的绘制裁剪,可以减少绘制时间。
使用View.setLayerType(View.LAYER_TYPE_HARDWARE, null)
将View的绘制硬件加速,可以提高绘制性能。
通过设置View.setDrawingCacheEnabled(true)
开启绘制缓存,可以减少绘制次数。
除了使用系统提供的View,我们还可以自定义View来实现特定的绘制效果。自定义View的绘制过程与系统View的绘制流程类似,只需要在onMeasure()
、onLayout()
和onDraw()
方法中实现自己的逻辑即可。
自定义View的绘制过程可以通过以下步骤来完成:
重写onMeasure()
方法,根据自己的需求计算出View的测量宽度和高度。
重写onLayout()
方法,根据测量结果和父容器的布局参数,确定View在父容器中的位置和大小。
重写onDraw()
方法,根据需要绘制自定义的内容。在onDraw()
方法中,可以使用Canvas
对象进行绘制操作,如绘制图形、绘制文本等。
可选地,重写onTouchEvent()
方法来处理触摸事件,实现交互功能。
在需要使用自定义View的地方,将其添加到布局文件中或者通过代码动态添加到布局中。
在View的绘制过程中,还有一些相关的回调方法可以帮助我们进行额外的处理:
onSizeChanged()
:当View的大小发生改变时调用,可以在这里进行一些与尺寸相关的操作。
onDetachedFromWindow()
:当View从窗口中移除时调用,可以在这里进行一些资源的释放和清理工作。
onAttachedToWindow()
:当View被添加到窗口中时调用,可以在这里进行一些初始化操作。
class CustomView(context: Context) : View(context) {
private val paint = Paint()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = 200 // 设置View的期望宽度
val desiredHeight = 200 // 设置View的期望高度
val width = resolveSize(desiredWidth, widthMeasureSpec) // 根据期望宽度和测量规格计算实际宽度
val height = resolveSize(desiredHeight, heightMeasureSpec) // 根据期望高度和测量规格计算实际高度
setMeasuredDimension(width, height) // 设置View的测量宽度和高度
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
// 在布局过程中不需要做额外的操作
}
override fun onDraw(canvas: Canvas) {
val centerX = width / 2f // 计算绘制圆的中心点X坐标
val centerY = height / 2f // 计算绘制圆的中心点Y坐标
val radius = min(centerX, centerY) // 计算绘制圆的半径,取宽度和高度的最小值
paint.color = Color.RED // 设置画笔颜色为红色
canvas.drawCircle(centerX, centerY, radius, paint) // 在画布上绘制一个圆形
}
override fun onTouchEvent(event: MotionEvent): Boolean {
// 在触摸事件中不需要做额外的操作
return super.onTouchEvent(event)
}
}
View的绘制是由Android系统的ViewRootImpl类中的performTraversals()方法触发的。下面是一部分ViewRootImpl类的源码,展示了View的绘制流程:
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks {
// ...
void performTraversals() {
// ...
if (mFirst) {
// 在首次绘制时执行一些初始化操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
} else {
// 在非首次绘制时执行布局操作
layoutRequested = mLayoutRequesters.size() > 0;
performLayout(lpChanged, mWidth, mHeight);
}
// 执行绘制操作
performDraw();
// ...
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
// ...
// 遍历View树执行测量操作
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// ...
}
private void performLayout(boolean changed, int left, int top, int right, int bottom) {
// ...
// 遍历View树执行布局操作
mView.layout(left, top, right, bottom);
// ...
}
private void performDraw() {
// ...
// 创建Canvas对象
Canvas canvas = mSurface.lockCanvas(frame);
try {
// 清空画布
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// 遍历View树执行绘制操作
mView.draw(canvas, this, mDrawingTime);
} finally {
// 解锁画布并提交绘制结果
mSurface.unlockCanvasAndPost(canvas);
}
// ...
}
// ...
}
在ViewRootImpl的performTraversals()方法中,首先会进行一些初始化操作,然后根据是否是首次绘制来执行测量操作或布局操作。接着,执行绘制操作。
在performMeasure()方法中,通过遍历View树,调用每个View的measure()方法来进行测量操作。
在performLayout()方法中,通过遍历View树,调用每个View的layout()方法来进行布局操作。
在performDraw()方法中,首先创建一个Canvas对象,并清空画布。然后,通过遍历View树,调用每个View的draw()方法来执行实际的绘制操作。最后,解锁画布并提交绘制结果。
这样,通过ViewRootImpl类的performTraversals()方法,系统会依次调用View的measure()、layout()和draw()方法,完成View的绘制流程。
测量操作是为了确定View的宽度和高度,以便在后续的布局(layout)和绘制(draw)过程中正确地确定View的位置和大小。在执行performMeasure时,会调用View的measure方法来进行具体的测量操作。
measure方法会根据View的布局参数(LayoutParams)
和父容器的约束条件
,计算出View的测量宽度和测量高度。这些测量值会被保存起来,供后续的布局和绘制使用。
因此,在performTraversals方法中执行performMeasure是为了保证在绘制过程中,View的宽度和高度是正确的,能够正确地进行布局和绘制操作。
View的测量宽度和测量高度是根据其布局参数(LayoutParams)和父容器的约束条件来计算的。具体的计算过程如下:
获取View的布局参数(LayoutParams):通过View.getLayoutParams()
方法可以获取到View的布局参数对象。
获取父容器的约束条件:父容器在布局过程中会给子View提供一些约束条件,例如父容器的宽度和高度、子View的边距等。通过父容器的MeasureSpec可以获取到这些约束条件。MeasureSpec是一个32位的整数,高2位表示测量模式(MeasureSpecMode),低30位表示测量大小(MeasureSpecSize)。
解析父容器的约束条件:通过MeasureSpec.getMode()
和MeasureSpec.getSize()
方法可以分别获取测量模式和测量大小。
根据测量模式和布局参数计算测量宽度和测量高度:
MeasureSpec.EXACTLY
,表示父容器对子View有精确的要求,此时测量大小就是父容器提供的测量大小。MeasureSpec.AT_MOST
,表示父容器对子View有最大的要求,此时测量大小可以是布局参数中的宽度和高度,但不能超过父容器提供的测量大小。MeasureSpec.UNSPECIFIED
,表示父容器对子View没有任何要求,此时测量大小可以是布局参数中的宽度和高度,也可以是子View的原始大小。将计算得到的测量宽度和测量高度保存起来:通过View.setMeasuredDimension()
方法可以将计算得到的测量宽度和测量高度保存起来,供后续的布局和绘制使用。
需要注意的是,View的测量过程是在View的measure方法中完成的,所以在自定义View时,可以重写measure方法来实现自定义的测量逻辑。
performLayout 方法的作用是根据测量结果和布局参数,计算出每个 View 的位置和尺寸,并将这些信息保存到各个 View 的 LayoutParams 对象中。
以下是 performLayout 方法的简化版本:
void performLayout(int parentWidth, int parentHeight) {
// 根据测量结果和布局参数计算 View 的位置和尺寸
layout(left, top, right, bottom);
// 递归调用子 View 的 performLayout 方法
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.performLayout(childWidth, childHeight);
}
}
特别地,来分析一下View.getWidth() 和 View.getMeasuredWidth()
**View.getWidth(): 这个方法返回的是 View 的实际宽度,也就是 View 在布局过程中最终确定下来的宽度。**它会受到布局参数和父容器的限制影响,因此它的值可能会随着布局的改变而改变。
**View.getMeasuredWidth(): 这个方法返回的是 View 在测量过程中计算出的宽度。**在绘制过程中,每个 View 需要经历测量(measure)、布局(layout)和绘制(draw)三个阶段。在测量阶段,系统会根据 View 的测量规格(MeasureSpec)计算出 View 的测量宽度。这个测量宽度可以通过 getMeasuredWidth() 方法来获取,它并不受布局参数和父容器的限制影响。
所以,View.getWidth() 返回的是实际宽度,受布局和父容器限制影响;而 View.getMeasuredWidth() 返回的是测量宽度,不受布局和父容器限制影响。
执行performDraw()方法的目的是就是为了将View的内容绘制到屏幕上,实现用户界面的显示。
下面是performDraw()方法的部分源码:
void performDraw() {
...
// 清除绘制缓存
if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.stopDrawing();
} else {
canvas.drawColor(mCurBackgroundColor);
}
// 绘制背景
if (!dirtyOpaque.isEmpty()) {
canvas.clipRect(dirtyOpaque, Op.REPLACE);
canvas.drawColor(mCurBackgroundColor);
}
// 绘制子View
mView.draw(canvas, this);
// 绘制装饰视图(如窗口标题栏、状态栏等)
if (mAttachInfo.mOverlay != null && !mAttachInfo.mOverlay.isEmpty()) {
mAttachInfo.mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 绘制焦点视图
if (mView.hasFocus()) {
View focusedView = mView.findFocus();
...
focusedView.dispatchDraw(canvas);
}
...
}
Thank you for your reading, best regards!