Android View绘制流程详解

在Android中,View是构成用户界面的基本元素之一。了解View的绘制流程对于开发人员来说非常重要,可以帮助他们更好地理解和优化应用程序的性能。

1. View的绘制过程

View的绘制过程可以总结为以下几个步骤:

  1. 测量(Measure):在绘制之前,View首先要进行测量,以确定它的大小。这一步是通过调用measure()方法实现的。在测量过程中,View会根据它的布局参数和父容器的约束条件,计算出自己的测量宽度和高度。

  2. 布局(Layout):在测量完成后,View进入布局阶段。在这个阶段,View会调用layout()方法来确定自己在父容器中的位置。布局过程中,View会根据父容器的布局参数和自身的测量结果,计算出自己的位置和大小。

  3. 绘制(Draw):在布局完成后,View进入绘制阶段。在这个阶段,View会调用draw()方法来绘制自己的内容。绘制过程中,View会根据自己的测量结果和布局结果,将自己的内容绘制到屏幕上。

2. View的绘制优化

为了提高应用程序的性能,我们可以采取一些优化措施来减少View的绘制次数和绘制时间:

  • 避免过多的嵌套布局,因为嵌套布局会增加测量和布局的时间消耗。

  • 使用ViewStub来延迟加载复杂的布局,只有在需要显示时才真正进行测量、布局和绘制。

  • 使用ViewGroup.setClipChildren(false)来关闭子View的绘制裁剪,可以减少绘制时间。

  • 使用View.setLayerType(View.LAYER_TYPE_HARDWARE, null)将View的绘制硬件加速,可以提高绘制性能。

  • 通过设置View.setDrawingCacheEnabled(true)开启绘制缓存,可以减少绘制次数。

3. View的自定义绘制

除了使用系统提供的View,我们还可以自定义View来实现特定的绘制效果。自定义View的绘制过程与系统View的绘制流程类似,只需要在onMeasure()onLayout()onDraw()方法中实现自己的逻辑即可。

自定义View的绘制过程可以通过以下步骤来完成:

  1. 重写onMeasure()方法,根据自己的需求计算出View的测量宽度和高度。

  2. 重写onLayout()方法,根据测量结果和父容器的布局参数,确定View在父容器中的位置和大小。

  3. 重写onDraw()方法,根据需要绘制自定义的内容。在onDraw()方法中,可以使用Canvas对象进行绘制操作,如绘制图形、绘制文本等。

  4. 可选地,重写onTouchEvent()方法来处理触摸事件,实现交互功能。

  5. 在需要使用自定义View的地方,将其添加到布局文件中或者通过代码动态添加到布局中。

4. View的绘制相关回调方法

在View的绘制过程中,还有一些相关的回调方法可以帮助我们进行额外的处理:

  • onSizeChanged():当View的大小发生改变时调用,可以在这里进行一些与尺寸相关的操作。

  • onDetachedFromWindow():当View从窗口中移除时调用,可以在这里进行一些资源的释放和清理工作。

  • onAttachedToWindow():当View被添加到窗口中时调用,可以在这里进行一些初始化操作。

5. 示例

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)
    }
}

6. 从源码的角度来分析

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()方法中,首先会进行一些初始化操作,然后根据是否是首次绘制来执行测量操作或布局操作。接着,执行绘制操作。

  1. 在performMeasure()方法中,通过遍历View树,调用每个View的measure()方法来进行测量操作。

  2. 在performLayout()方法中,通过遍历View树,调用每个View的layout()方法来进行布局操作。

  3. 在performDraw()方法中,首先创建一个Canvas对象,并清空画布。然后,通过遍历View树,调用每个View的draw()方法来执行实际的绘制操作。最后,解锁画布并提交绘制结果。

这样,通过ViewRootImpl类的performTraversals()方法,系统会依次调用View的measure()、layout()和draw()方法,完成View的绘制流程。

1. Why performMeasure?

测量操作是为了确定View的宽度和高度,以便在后续的布局(layout)和绘制(draw)过程中正确地确定View的位置和大小。在执行performMeasure时,会调用View的measure方法来进行具体的测量操作。

measure方法会根据View的布局参数(LayoutParams)父容器的约束条件,计算出View的测量宽度和测量高度。这些测量值会被保存起来,供后续的布局和绘制使用。

因此,在performTraversals方法中执行performMeasure是为了保证在绘制过程中,View的宽度和高度是正确的,能够正确地进行布局和绘制操作。

View的测量宽度和测量高度是根据其布局参数(LayoutParams)和父容器的约束条件来计算的。具体的计算过程如下:

  1. 获取View的布局参数(LayoutParams):通过View.getLayoutParams()方法可以获取到View的布局参数对象。

  2. 获取父容器的约束条件:父容器在布局过程中会给子View提供一些约束条件,例如父容器的宽度和高度、子View的边距等。通过父容器的MeasureSpec可以获取到这些约束条件。MeasureSpec是一个32位的整数,高2位表示测量模式(MeasureSpecMode),低30位表示测量大小(MeasureSpecSize)。

  3. 解析父容器的约束条件:通过MeasureSpec.getMode()MeasureSpec.getSize()方法可以分别获取测量模式和测量大小。

  4. 根据测量模式和布局参数计算测量宽度和测量高度:

    • 如果测量模式是MeasureSpec.EXACTLY,表示父容器对子View有精确的要求,此时测量大小就是父容器提供的测量大小。
    • 如果测量模式是MeasureSpec.AT_MOST,表示父容器对子View有最大的要求,此时测量大小可以是布局参数中的宽度和高度,但不能超过父容器提供的测量大小。
    • 如果测量模式是MeasureSpec.UNSPECIFIED,表示父容器对子View没有任何要求,此时测量大小可以是布局参数中的宽度和高度,也可以是子View的原始大小。
  5. 将计算得到的测量宽度和测量高度保存起来:通过View.setMeasuredDimension()方法可以将计算得到的测量宽度和测量高度保存起来,供后续的布局和绘制使用。

需要注意的是,View的测量过程是在View的measure方法中完成的,所以在自定义View时,可以重写measure方法来实现自定义的测量逻辑。

2. Why performLayout ?

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() 返回的是测量宽度,不受布局和父容器限制影响。

3. Why performDraw?

执行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!

你可能感兴趣的:(Android,夯实基础,android)