在上一篇文章中我们学习了如何在~中添加一个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是连接WindowManager和DecorView的纽带。
3.View三大流程均是藉由ViewRoot来完成的。
基础认知2
1.measure过程决定View的宽高,当这个过程执行完成,可以通过getMeasureWidth和getMeasureHeight来获取View测量后的宽高,至于获取的结果究竟和实际情况是否相符,这个是有特殊情况的,但大部分时间是相符的。
2.Layout过程决定View的四个顶点坐标和实际View的宽高,完成之后可以通过getTop,getBottom,getLeft,getRight来拿到View四个顶点的位置。并能通过getWidth(),getHeaght()来获取最终实际的宽高。
3.Draw过程决定View的显示,只有draw完成之后View内从才能显示到屏幕上。
基础认知3
1.DecorView是顶级View,它是一个FrameLayout。
2.DecorView它内部包含一个竖直方向的LinearLayout。这个~里有上下俩部分(android主题相关),上面是标题栏,下面是内容栏。
3.在Activity中我们通过setContentView所设置的布局文件其实是被加到内容栏之中的。内容栏的系统id是R.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通过将specMode和specSize打包成一个int值来避免过多的对象内存分配
4.MeasureSpec不仅仅是由LayoutParams来决定,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。但DecorView有点特殊,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
5.对于我们普通布局中的View,它的Measure是由ViewGroup传递而来。受父控件的宽高影响,同时也受padding,margin影响。
6.SpecMode有三种类型:
a.unspecified
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
b.exactly
父容器已经检测出View所需要的精确大小,这个时候View最终代销就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这俩种模式。
c.at_most
父容器指定了一个可用大小即specSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于layoutParams中的wrap_content。
performMeasure
在执行performMeasure时,会调用View的measure方法来进行具体的测量操作。我们如果自定义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的,可以看一下。
-
解析LayoutParams:
- 父布局首先会解析子布局的LayoutParams,这些参数通常是在XML布局文件中定义的,或者在代码中动态设置的。
- LayoutParams包含了子布局的宽度和高度信息,这些信息可以是具体的数值(如100dp),也可以是特殊的值,如
match_parent或wrap_content。
-
确定子布局的MeasureSpec模式:
- 对于宽度和高度,父布局会根据LayoutParams中的值以及自身的MeasureSpec模式来决定子布局的MeasureSpec模式。
- 如果LayoutParams中的宽度或高度是具体的数值,那么子布局的MeasureSpec模式通常会被设置为
EXACTLY,并且大小就是该具体数值。 - 如果LayoutParams中的宽度或高度是
match_parent,那么子布局的MeasureSpec模式通常会继承父布局的MeasureSpec模式,并且大小会被设置为父布局在该方向上的剩余空间(考虑到边距和填充)。 - 如果LayoutParams中的宽度或高度是
wrap_content,那么子布局的MeasureSpec模式通常会被设置为AT_MOST,并且大小会被设置为父布局在该方向上的剩余空间(同样考虑到边距和填充),这样子布局就可以根据其内容来调整自己的大小,但不会超过父布局允许的最大尺寸。
-
考虑特殊情况:
- 在某些特殊情况下,如ScrollView或ListView等滚动容器中,子布局的MeasureSpec模式可能会有所不同,因为这些容器需要对子布局进行特殊的测量和处理。
- 如果父布局的MeasureSpec模式是
UNSPECIFIED(未指定),那么子布局的MeasureSpec模式也可能会是UNSPECIFIED,或者父布局可能会根据自己的逻辑来设置一个默认的MeasureSpec。
-
调用子布局的measure方法:
- 一旦确定了子布局的MeasureSpec,父布局就会调用子布局的
measure方法,并将计算出的MeasureSpec作为参数传递给它。 - 子布局随后会根据这个MeasureSpec来测量自己的大小,并调用
setMeasuredDimension方法来设置自己的实际宽度和高度。
- 一旦确定了子布局的MeasureSpec,父布局就会调用子布局的
performLayout
当布局又下向上执行完measure后,view就会执行performlayout方法去确定view的具体位置,依次执行的方法为performlayout->layout->onlayout,这通常涉及到计算子View的四个边界值:mLeft、mTop、mRight和mBottom,这些值表示子View相对于父ViewGroup的位置。最后执行下面的代码确定子view的位置。
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
讲到这里突然提一下view的刷新方法。其中requestLayout与invalidate的区别:
-
调用
invalidate()方法:-
invalidate()方法用于标记View为“需要重绘”状态。当调用此方法时,系统会安排在下一个绘制周期中重绘该View。如果View是可见的,并且没有被其他View遮挡,那么它的onDraw()方法将在下一个绘制周期中被调用。
-
-
调用
postInvalidate()方法:-
postInvalidate()方法与invalidate()类似,但它允许在非UI线程中安全地请求View的重绘。当在非UI线程中需要更新UI时,可以使用此方法。它会将刷新请求放入主线程的消息队列中,由主线程在下一个消息循环中处理。
-
-
调用
requestLayout()方法:- 当View的布局参数发生变化(如大小、边距等)时,需要调用此方法重新测量和布局View。这也会触发View的绘制过程。注意,
requestLayout()通常与布局变化相关,而不是内容变化。
- 当View的布局参数发生变化(如大小、边距等)时,需要调用此方法重新测量和布局View。这也会触发View的绘制过程。注意,
onDraw
在View的大小和位置都确定之后,系统会调用onDraw方法来绘制View的内容。这个方法会传递一个Canvas对象给View,View可以使用这个Canvas来绘制自己的外观,这个方法大家随意操作就行了,View是否炫酷就靠你的绘画能力了。
综上所述,View的绘制流程可以概括为:首先通过onMeasure方法测量View的大小,然后通过onLayout方法(对于ViewGroup而言)确定View在父容器中的位置,最后通过onDraw方法在画布上绘制View的内容。这个过程是Android视图系统确保View正确显示和交互的基础。