Android-View的绘制(终)

在上一篇文章中我们学习了如何在~中添加一个View,但是View的大小、形状、位置等确还不确定,接下来我们就学习一下View是如何绘制的。

View的绘制主要应用了三个方法:

1.performMeasure->measure->onMesuer

在绘制之前,View首先要进行测量,以确定它的大小。View会根据它的布局参数和父容器的测量结果,计算出自己的测量宽度和高度。

performLayout->layout->onLayout

在测量完成后,View进入布局阶段。View会根据父容器的布局参数和自身的测量结果,计算出自己的位置和大小。

performDraw->draw->onDraw

在布局完成后,View进入绘制阶段。View会根据自己的测量结果和布局结果,将自己的内容绘制到屏幕上。

在了解这三个方法之前,我们需要了解一些基础认知。

基础认知1
1.在ActivityThread中,当Acitivty对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和~建立关联。
2.ViewRootImpl是连接WindowManagerDecorView的纽带。
3.View三大流程均是藉由ViewRoot来完成的。

基础认知2
1.measure过程决定View的宽高,当这个过程执行完成,可以通过getMeasureWidthgetMeasureHeight来获取View测量后的宽高,至于获取的结果究竟和实际情况是否相符,这个是有特殊情况的,但大部分时间是相符的。
2.Layout过程决定View的四个顶点坐标和实际View的宽高,完成之后可以通过getTopgetBottomgetLeftgetRight来拿到View四个顶点的位置。并能通过getWidth(),getHeaght()来获取最终实际的宽高。
3.Draw过程决定View显示,只有draw完成之后View内从才能显示到屏幕上。

基础认知3
1.DecorView是顶级View,它是一个FrameLayout
2.DecorView它内部包含一个竖直方向的LinearLayout。这个~里有上下俩部分(android主题相关),上面是标题栏,下面是内容栏。
3.在Activity中我们通过setContentView所设置的布局文件其实是被加到内容栏之中的。内容栏的系统idR.id.content,这个内容栏和DecorView一样也是个FramentLayout
4.获取内容栏:findViewById(R.android.id.content),获取内容栏中的View,``content.getChildAt(0)

基本认识4
1.MeasureSpec很大程度上决定了一个View的尺寸规格(View的尺寸规格还受父布局影响)
2.MeasureSpec是一个32位的int值,高2位代表SpecMode—测量模式,低30位代表SpecSize—在某种测量模式下的规格大小
3.MeasureSpec通过将specModespecSize打包成一个int值来避免过多的对象内存分配
4.MeasureSpec不仅仅是由LayoutParams来决定,LayoutParams需要和父容器一起才能决定ViewMeasureSpec,从而进一步决定View的宽高。但DecorView有点特殊,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。MeasureSpec一旦确定后,onMeasure就可以确定View的测量宽/高。
5.对于我们普通布局中的View,它的Measure是由ViewGroup传递而来。受父控件的宽高影响,同时也受paddingmargin影响。
6.SpecMode有三种类型:
a.unspecified
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
b.exactly
父容器已经检测出View所需要的精确大小,这个时候View最终代销就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这俩种模式。
c.at_most
父容器指定了一个可用大小即specSizeView的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于layoutParams中的wrap_content

performMeasure

在执行performMeasure时,会调用Viewmeasure方法来进行具体的测量操作。我们如果自定义View的话则需要实现onMeasure方法对View以及他的子View进行测量,然后通过 setMeasuredDimension 方法设置测量后的尺寸。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int desiredWidth = 200;
        int desiredHeight = 200;

        if (widthMode == MeasureSpec.EXACTLY) {
            desiredWidth = widthSize;
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            desiredHeight = heightSize;
        }

        setMeasuredDimension(desiredWidth, desiredHeight);
    }
}

我们前面也说过view的测量主要依靠自身的View的布局参数(LayoutParams,通过View.getLayoutParams()方法可以获取)和父布局的测量结果(MeasureSpec)来决定的。那么父布局是如何根据这俩个餐数据确定子布局的specMode的,可以看一下。

  1. 解析LayoutParams
    • 父布局首先会解析子布局的LayoutParams,这些参数通常是在XML布局文件中定义的,或者在代码中动态设置的。
    • LayoutParams包含了子布局的宽度和高度信息,这些信息可以是具体的数值(如100dp),也可以是特殊的值,如match_parentwrap_content
  2. 确定子布局的MeasureSpec模式
    • 对于宽度和高度,父布局会根据LayoutParams中的值以及自身的MeasureSpec模式来决定子布局的MeasureSpec模式。
    • 如果LayoutParams中的宽度或高度是具体的数值,那么子布局的MeasureSpec模式通常会被设置为EXACTLY,并且大小就是该具体数值。
    • 如果LayoutParams中的宽度或高度是match_parent,那么子布局的MeasureSpec模式通常会继承父布局的MeasureSpec模式,并且大小会被设置为父布局在该方向上的剩余空间(考虑到边距和填充)。
    • 如果LayoutParams中的宽度或高度是wrap_content,那么子布局的MeasureSpec模式通常会被设置为AT_MOST,并且大小会被设置为父布局在该方向上的剩余空间(同样考虑到边距和填充),这样子布局就可以根据其内容来调整自己的大小,但不会超过父布局允许的最大尺寸。
  3. 考虑特殊情况
    • 在某些特殊情况下,如ScrollView或ListView等滚动容器中,子布局的MeasureSpec模式可能会有所不同,因为这些容器需要对子布局进行特殊的测量和处理。
    • 如果父布局的MeasureSpec模式是UNSPECIFIED(未指定),那么子布局的MeasureSpec模式也可能会是UNSPECIFIED,或者父布局可能会根据自己的逻辑来设置一个默认的MeasureSpec。
  4. 调用子布局的measure方法
    • 一旦确定了子布局的MeasureSpec,父布局就会调用子布局的measure方法,并将计算出的MeasureSpec作为参数传递给它。
    • 子布局随后会根据这个MeasureSpec来测量自己的大小,并调用setMeasuredDimension方法来设置自己的实际宽度和高度。

performLayout

当布局又下向上执行完measure后,view就会执行performlayout方法去确定view的具体位置,依次执行的方法为performlayout->layout->onlayout,这通常涉及到计算子View的四个边界值:mLeftmTopmRightmBottom,这些值表示子View相对于父ViewGroup的位置。最后执行下面的代码确定子view的位置。

 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);

讲到这里突然提一下view的刷新方法。其中requestLayoutinvalidate的区别:

  1. 调用invalidate()方法
    • invalidate()方法用于标记View为“需要重绘”状态。当调用此方法时,系统会安排在下一个绘制周期中重绘该View。如果View是可见的,并且没有被其他View遮挡,那么它的onDraw()方法将在下一个绘制周期中被调用。
  2. 调用postInvalidate()方法
    • postInvalidate()方法与invalidate()类似,但它允许在非UI线程中安全地请求View的重绘。当在非UI线程中需要更新UI时,可以使用此方法。它会将刷新请求放入主线程的消息队列中,由主线程在下一个消息循环中处理。
  3. 调用requestLayout()方法
    • 当View的布局参数发生变化(如大小、边距等)时,需要调用此方法重新测量和布局View。这也会触发View的绘制过程。注意,requestLayout()通常与布局变化相关,而不是内容变化。

onDraw

在View的大小和位置都确定之后,系统会调用onDraw方法来绘制View的内容。这个方法会传递一个Canvas对象给View,View可以使用这个Canvas来绘制自己的外观,这个方法大家随意操作就行了,View是否炫酷就靠你的绘画能力了。

综上所述,View的绘制流程可以概括为:首先通过onMeasure方法测量View的大小,然后通过onLayout方法(对于ViewGroup而言)确定View在父容器中的位置,最后通过onDraw方法在画布上绘制View的内容。这个过程是Android视图系统确保View正确显示和交互的基础。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容