Android(安卓)- View的绘制流程一(measure)
16lz
2022-06-16
该博文所用的demo结构图: 对应的代码: MainActivity.java:
那么,这些view对象都是怎么绘制到屏幕上的呢? 核心逻辑都在ViewRoot的performTraversals()方法中,主要分为三个阶段: (该图截取自Android内核剖析一书,谢谢作者) 第一个阶段是measure,第二个阶段是layout,第三个阶段是draw 一、mesarue performTraversals()方法中关于measure的代码(进行了简化,仅为了解大致流程)如下: 如下: private void performTraversals() { final View host = mView; // mView就是一个DecorView对象,所以,view的绘制是从根view - -DecorView开始的 int desiredWindowWidth; int desiredWindowHeight; int childWidthMeasureSpec; int childHeightMeasureSpec; //获取手机屏幕分辨率 DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; 1、childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); //传入的参数为手机屏幕的宽和高,lp.width和lp.height (一般为MATCH_PARENT) private int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); public static int makeMeasureSpec(int size, int mode) { return size + mode; } break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; } 2、host.measure(childWidthMeasureSpec, childHeightMeasureSpec); } 上边就是从根view开始绘制整个view树的主要代码,比较重要的是第1和第2处 首先,第1处: 由屏幕的宽和高分别与MeasureSpec.EXACTLY进行组合得到childWidthMeasureSpec 和childHeightMeasureSpec的值,逻辑比较简单,主要是涉及到一个MeasureSpec类。它是view的一个静态内部类,看它的注释: 一个measurespec封装了父视图对子视图在长度或宽度上的要求。一个measurespec由大小和模式组成(使用makeMeasureSpec方法获取)。 其中,模式有三个可选值: UNSPECIFIED:代表父视图对子视图在长度或宽度上不施加任何约束,子视图可以是任何它想要的大小 EXACTLY:代表父视图已经对子视图在长度或宽度上确定了一个准确的尺寸,不管子视图想要多大,它都会受到这个尺寸的限制 AT_MOST:代表子视图最多只能是设置的大小 在measurespec类中,主要有三个常用的方法: public static int makeMeasureSpec(int size, int mode) //该方法用于将一个具体的尺寸值和一个mode(比如EXACTLY)组合成一个measureSpec public static int getMode(int measureSpec) //该方法用于从一个measureSpec中取出相应的mode(比如EXACTLY) public static int getSize(int measureSpec) //该方法用于从一个measureSpec中取出具体的size 然后,第2处: 将第1处计算得到的childWidthMeasureSpec 和childHeightMeasureSpec作为参数,执行host.measure(childWidthMeasureSpec,childHeightMeasureSpec); (代码经简化,仅为理解大致流程) public final void measure(int widthMeasureSpec, int heightMeasureSpec) { onMeasure(widthMeasureSpec, heightMeasureSpec); } measure()方法定义在view中,是final类型的,子类不能重写,它里边调用了onMeasure()方法,在这里,如果是非ViewGroup类型的view对象,则执行view类中的onMeasure方法,而对于ViewGroup类型的对象比如FrameLayout和LinearLayout来讲,因为ViewGroup类中并没有重写onMeasure()方法,所以都将执行自己类中的onMeasure()方法,它们的大致逻辑如下: protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); //遍历,执行 measureChildWithMargins(child, ... ... ) 方法(定义在ViewGroup类中) measureChildWithMargins(child, ... ... protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } 最终在measureChildWithMargins方法中又调用了measure方法,所以view的绘制是一个从根view到子view递归的过程。 如上文的图一和图二所示,在该博文的demo中,首先执行根view -- mDecor的measure方法,在mDecor的父类FrameLayout的onMeasure方法里进行遍历,然后mDecor的子类 ——向mDecor里添加的窗口布局(图一中为R.layout.screen_simple,图二中为R.layout.screen_title)的measure方法得到执行,再在R.layout.screen_simple或R.layout.screen_title的onMeasure方法中进行遍历,执行到id为content的布局对应的measure方法,... ... ,直到该博文demo中的MyCustomLinearLayoutA的measure和onMeasure方法。 由于从DecorView到MyCustomLinearLayoutA进行遍历的过程中,还涉及到状态栏绘制的问题,本篇博文就不详细写了,但需要关注一点,就是onMeasure方法接收的参数的问题,这也是我们自定义view重写onMeasure方法时遇到的问题之一。 从performTraversals()方法中的host.measure(childWidthMeasureSpec,childHeightMeasureSpec)开始,在遍历的整个过程中,measure和onMeasure方法接收的参数都是一个measurespec,它封装了父视图对子视图在长度或宽度上的要求,我们可以通过MeasureSpec的getSize和getMode方法解析出对应的size和mode。 performTraversals()方法中host.measure方法接收的参数对应的size ——MeasureSpec.getSize(childWidthMeasureSpec)和MeasureSpec.getSize(childHeightMeasureSpec)正好是屏幕的宽和高 运行上述demo,打印log如下(测试手机1920*1080,单位全部换算为dp): 在设置全屏和非全屏的情况下,MyCustomLinearLayoutA的onMeasure方法接收的参数对应的size ——MeasureSpec.getSize(widthMeasureSpec)和MeasureSpec.getSize(heightMeasureSpec)正好分别是上文图一和图二中红色部分即id为content的布局的宽和高(参考MainActivity的onPause方法)。 那是不是每一个view的onMeasure方法接收的参数对应的size都是其父视图的宽高呢?带着这个问题,我们就以MyCustomLinearLayoutA为例,从onMeasure()方法开始分析一个ViewGroup类型的视图遍历—measure子视图的详细过程。
LinearLayout类中的onMeasure()方法:
所以,我们自定义view对象,重写onMeasure方法的目的就是按照我们的方式计算并设置view对象的宽和高,比如: @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(201, 801); } 这样的话就把View默认的measure流程覆盖掉了,不管在布局文件中定义的大小是多少,最终在界面上显示的大小都将会是201*801. 可是,上述代码的意义是什么呢?或者说在什么情况下我们需要重写onMeasure方法呢? 在不想要遵循view的默认measure流程时,或者需要动态调整布局,不太容易在xml文件中操作时, 我们可以考虑重写view的onMeasure方法。
在上文中,我们提到,view的measure过程使用到了xml文件中的layout_width、layout_height、layout_weight、margin和padding这些属性,那view的绘制过程的第二步layout和第三步draw是否会使用到xml文件中的其他属性呢?回答是肯定的。
虽然measure翻译为测量,layout翻译为布局,但其实这两个步骤都是在计算,只不过measure过程是在计算view的大小,layout过程是在计算view的坐标而已。view绘制的第二步 — layout ,核心就是根据xml文件中的gravity属性和第一步 — measure所得到的宽和高的值计算出view对象的坐标。大小和位置都确定了,就可以进行第三步 —draw了。
<span style="font-family:Microsoft YaHei;">public class MainActivity extends Activity {private int desiredWindowWidth;private int desiredWindowHeight;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//获取屏幕的宽高,单位为dp desiredWindowWidth = Measurement.getScreenWidth(this); desiredWindowHeight = Measurement.getScreenHeight(this); Log.d("HWGT", "屏幕宽..=.." + desiredWindowWidth + "....屏幕高..=.." + desiredWindowHeight);}@Overrideprotected void onPause() {super.onPause();//获取状态栏的高度(标题栏+content区域的top坐标)Rect frame = new Rect();getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);int statusBarHeight = Measurement.px2dip(this, frame.top); //获取 标题栏+content 区域的高度int titleAndContentHeight = Measurement.px2dip(this, frame.height());//获取content区域的top坐标 int tempContentTop = getWindow().findViewById(Window.ID_ANDROID_CONTENT).getTop(); int contentTop = Measurement.px2dip(this, tempContentTop); //标题栏的高度 = content区域的top坐标 - 状态栏的高度int titleBarHeight = contentTop - statusBarHeight;Log.d("HWGT", "titleBarHeight..=.."+titleBarHeight+"....contentTop..=.."+contentTop+"....statusBarHeight..=.." + statusBarHeight );}}</span>activity_main.xml:
<span style="font-family:Microsoft YaHei;"><com.hwgt.drawingprocessofview.ui.MyCustomLinearLayoutA xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context="com.hwgt.drawingprocessofview.MainActivity" > <com.hwgt.drawingprocessofview.ui.MyCustomTextViewAandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="13dp"android:layout_gravity="center_horizontal"android:text="@string/hello_world" /><com.hwgt.drawingprocessofview.ui.MyCustomLinearLayoutB android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="13dp"><com.hwgt.drawingprocessofview.ui.MyCustomButtonA android:layout_width="111dp" android:layout_height="wrap_content" android:layout_marginLeft="31dp" android:text="@string/ok"/></com.hwgt.drawingprocessofview.ui.MyCustomLinearLayoutB></com.hwgt.drawingprocessofview.ui.MyCustomLinearLayoutA></span>Measurement.java:
<span style="font-family:Microsoft YaHei;">public class Measurement {public static int px2dip(Context context, float pxValue) {final float scale = context.getResources().getDisplayMetrics().density;return (int) (pxValue / scale + 0.5f);}public static int getScreenWidth(Context context) {WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics dm = new DisplayMetrics();manager.getDefaultDisplay().getMetrics(dm);return px2dip(context, dm.widthPixels);}public static int getScreenHeight(Context context) {WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics dm = new DisplayMetrics();manager.getDefaultDisplay().getMetrics(dm);return px2dip(context, dm.heightPixels);}}</span>MyCustomLinearLayoutA.java、MyCustomTextViewA.java、MyCustomLinearLayoutB.java 和MyCustomButtonA.java类似,构造函数省略了,onMeasure()方法中的处理也一样:
<span style="font-family:Microsoft YaHei;">public class MyCustomLinearLayoutA extends LinearLayout ... ...public class MyCustomTextViewA extends TextView ... ...public class MyCustomLinearLayoutB extends LinearLayout ... ...public class MyCustomButtonA extends Button {@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec);int heightMeasureSpecSize = MeasureSpec.getSize(heightMeasureSpec);Log.d("HWGT", "ButtonA: widthMeasureSpecSize..=.."+Measurement.px2dip(getContext(), widthMeasureSpecSize) + "....heightMeasureSpecSize..=.."+Measurement.px2dip(getContext(), heightMeasureSpecSize));}}</span>Activity 的setContentView() 方法执行后会调用到PhoneWindow 的 setContentView()方法 当第一次执行MainActivity 的setContentView(R.layout.activity_main) 时,在PhoneWindow的setContentView(intlayoutResID)方法中会进行以下操作: 1、调用installDecor() -->generateDecor()方法创建一个DecorView(FrameLayout的子类)对象mDecor 2、调用generateLayout(DecorViewdecor)方法, 该方法主要逻辑为: A、根据requestFreature()和Activity节点的android:theme=""等值选择相应的窗口布局文件添加进第一步创建的mDecor对象中—— decor.addView(in,newViewGroup.LayoutParams(MATCH_PARENT,MATCH_PARENT));(in即为相应的布局文件,并且,窗口布局是填充父窗体mDecor的) Activity比较常用的窗口布局文件有R.layout.screen_title 和R.layout.screen_simple R.layout.screen_title :
<span style="font-family:Microsoft YaHei;"><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:fitsSystemWindows="true"> <FrameLayout android:layout_width="match_parent" android:layout_height="?android:attr/windowTitleSize" style="?android:attr/windowTitleBackgroundStyle"> <TextView android:id="@android:id/title" style="?android:attr/windowTitleStyle" android:background="@null" android:fadingEdge="horizontal" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout></span>R.layout.screen_simple:
<span style="font-family:Microsoft YaHei;"><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/content" android:fitsSystemWindows="true" android:foregroundInsidePadding="false" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </span>这两个布局都包含一个id为content的FrameLayout布局,比如我们设置了requestWindowFeature(Window.FEATURE_NO_TITLE);或<item name="android:windowNoTitle">true</item>时,R.layout.screen_simple将被添加到mDecor中。 B、返回 通过findViewById()找到的id为content的布局mContentParent 3、执行mLayoutInflater.inflate(layoutResID,mContentParent); 将我们的R.layout.activity_main 布局添加到 mContentParent 中。 所以,mDecor对象就是应用程序窗口的根view,该demo的效果图和其涉及到的相关布局的关系如下图: 紫色框代表根view -- mDecor 蓝色框代表generateLayout(mDecor)方法中向mDecor里添加的布局,图一中为R.layout.screen_simple,图二中为R.layout.screen_title (状态栏是绘制在紫色框mDecor里还是蓝色框里还有待研究,这里假设状态栏的父窗体就是mDecor,不影响对view绘制的理解) 红色框代表R.layout.screen_simple 或R.layout.screen_title 中 id为content的布局 --mContentParent 绿色框代表R.layout.activity_main 白色框则是标题栏
那么,这些view对象都是怎么绘制到屏幕上的呢? 核心逻辑都在ViewRoot的performTraversals()方法中,主要分为三个阶段: (该图截取自Android内核剖析一书,谢谢作者) 第一个阶段是measure,第二个阶段是layout,第三个阶段是draw 一、mesarue performTraversals()方法中关于measure的代码(进行了简化,仅为了解大致流程)如下: 如下: private void performTraversals() { final View host = mView; // mView就是一个DecorView对象,所以,view的绘制是从根view - -DecorView开始的 int desiredWindowWidth; int desiredWindowHeight; int childWidthMeasureSpec; int childHeightMeasureSpec; //获取手机屏幕分辨率 DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; 1、childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); //传入的参数为手机屏幕的宽和高,lp.width和lp.height (一般为MATCH_PARENT) private int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); public static int makeMeasureSpec(int size, int mode) { return size + mode; } break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; } 2、host.measure(childWidthMeasureSpec, childHeightMeasureSpec); } 上边就是从根view开始绘制整个view树的主要代码,比较重要的是第1和第2处 首先,第1处: 由屏幕的宽和高分别与MeasureSpec.EXACTLY进行组合得到childWidthMeasureSpec 和childHeightMeasureSpec的值,逻辑比较简单,主要是涉及到一个MeasureSpec类。它是view的一个静态内部类,看它的注释: 一个measurespec封装了父视图对子视图在长度或宽度上的要求。一个measurespec由大小和模式组成(使用makeMeasureSpec方法获取)。 其中,模式有三个可选值: UNSPECIFIED:代表父视图对子视图在长度或宽度上不施加任何约束,子视图可以是任何它想要的大小 EXACTLY:代表父视图已经对子视图在长度或宽度上确定了一个准确的尺寸,不管子视图想要多大,它都会受到这个尺寸的限制 AT_MOST:代表子视图最多只能是设置的大小 在measurespec类中,主要有三个常用的方法: public static int makeMeasureSpec(int size, int mode) //该方法用于将一个具体的尺寸值和一个mode(比如EXACTLY)组合成一个measureSpec public static int getMode(int measureSpec) //该方法用于从一个measureSpec中取出相应的mode(比如EXACTLY) public static int getSize(int measureSpec) //该方法用于从一个measureSpec中取出具体的size 然后,第2处: 将第1处计算得到的childWidthMeasureSpec 和childHeightMeasureSpec作为参数,执行host.measure(childWidthMeasureSpec,childHeightMeasureSpec); (代码经简化,仅为理解大致流程) public final void measure(int widthMeasureSpec, int heightMeasureSpec) { onMeasure(widthMeasureSpec, heightMeasureSpec); } measure()方法定义在view中,是final类型的,子类不能重写,它里边调用了onMeasure()方法,在这里,如果是非ViewGroup类型的view对象,则执行view类中的onMeasure方法,而对于ViewGroup类型的对象比如FrameLayout和LinearLayout来讲,因为ViewGroup类中并没有重写onMeasure()方法,所以都将执行自己类中的onMeasure()方法,它们的大致逻辑如下: protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); //遍历,执行 measureChildWithMargins(child, ... ... ) 方法(定义在ViewGroup类中) measureChildWithMargins(child, ... ... protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } 最终在measureChildWithMargins方法中又调用了measure方法,所以view的绘制是一个从根view到子view递归的过程。 如上文的图一和图二所示,在该博文的demo中,首先执行根view -- mDecor的measure方法,在mDecor的父类FrameLayout的onMeasure方法里进行遍历,然后mDecor的子类 ——向mDecor里添加的窗口布局(图一中为R.layout.screen_simple,图二中为R.layout.screen_title)的measure方法得到执行,再在R.layout.screen_simple或R.layout.screen_title的onMeasure方法中进行遍历,执行到id为content的布局对应的measure方法,... ... ,直到该博文demo中的MyCustomLinearLayoutA的measure和onMeasure方法。 由于从DecorView到MyCustomLinearLayoutA进行遍历的过程中,还涉及到状态栏绘制的问题,本篇博文就不详细写了,但需要关注一点,就是onMeasure方法接收的参数的问题,这也是我们自定义view重写onMeasure方法时遇到的问题之一。 从performTraversals()方法中的host.measure(childWidthMeasureSpec,childHeightMeasureSpec)开始,在遍历的整个过程中,measure和onMeasure方法接收的参数都是一个measurespec,它封装了父视图对子视图在长度或宽度上的要求,我们可以通过MeasureSpec的getSize和getMode方法解析出对应的size和mode。 performTraversals()方法中host.measure方法接收的参数对应的size ——MeasureSpec.getSize(childWidthMeasureSpec)和MeasureSpec.getSize(childHeightMeasureSpec)正好是屏幕的宽和高 运行上述demo,打印log如下(测试手机1920*1080,单位全部换算为dp): 在设置全屏和非全屏的情况下,MyCustomLinearLayoutA的onMeasure方法接收的参数对应的size ——MeasureSpec.getSize(widthMeasureSpec)和MeasureSpec.getSize(heightMeasureSpec)正好分别是上文图一和图二中红色部分即id为content的布局的宽和高(参考MainActivity的onPause方法)。 那是不是每一个view的onMeasure方法接收的参数对应的size都是其父视图的宽高呢?带着这个问题,我们就以MyCustomLinearLayoutA为例,从onMeasure()方法开始分析一个ViewGroup类型的视图遍历—measure子视图的详细过程。
LinearLayout类中的onMeasure()方法:
<span style="font-family:Microsoft YaHei;">protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); }}</span>根据LinearLayout的布局方向调用measureVertical或measureHorizontal,下面以本文demo中MyCustomLinearLayoutA为例分析竖直方向的measureVertical。 该方法主要分为两个步骤(参考Android内核剖析第13章 — view工作原理): 一、进行遍历,跳过那些 lp.weight > 0 的子视图,调用measureChildBeforeLayout ----> measureChildWithMargins 方法对子视图进行measure,之后调用child.getMeasuredHeight()获取子视图的最终高度并添加到mTotalLength中。 二、把父视图剩余的高度按照weight大小均匀分配给子视图 本文只为分析measure的流程,所以暂时将weight>0的视图的测量过程省略掉了 voidmeasureVertical(int widthMeasureSpec, int heightMeasureSpec) { //该变量用来存储所有子view的高度 mTotalLength = 0; //获取子view的个数 final int count = getVirtualChildCount(); //从父view传递给MyCustomLinearLayoutA的MeasureSpec中获取宽和高的mode,这里都为EXACTLY final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); for (int i = 0; i < count; ++i) { //totalWeight == 0 ? mTotalLength : 0 ,这里跳过了weight>0的子视图,所以值为mTotalLength(在对MyCustomTextViewA进行measure时,值为0,另外,测量的是竖直方向,所以第4个参数直接传入0) measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec,totalWeight == 0 ? mTotalLength : 0); voidmeasureChildBeforeLayout(View child, int childIndex,int widthMeasureSpec, int totalWidth, int heightMeasureSpec,int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth,heightMeasureSpec, totalHeight); //(在对MyCustomTextViewA进行measure时,parentWidthMeasureSpec 和parentHeightMeasureSpec为父view传递给 // MyCustomLinearLayoutA的WidthMeasureSpec 和HeightMeasureSpec,widthUsed 和heightUsed 为MyCustomTextViewA // 已经使用的水平和竖直方向的尺寸,这里都为0) protected voidmeasureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { // 首先,拿到 child 即MyCustomTextViewA的布局参数 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // 然后,计算childWidthMeasureSpec和childHeightMeasureSpec的值 final int childWidthMeasureSpec =getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed, lp.width); final int childHeightMeasureSpec =getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height); // 第一个参数:是MyCustomLinearLayoutA的父view — id为content的布局传递来的WidthMeasureSpec // 我们可以通过MeasureSpec.getSize 和MeasureSpec.getMode 得到相应的size和mode(360dpMeasureSpec.EXACTLY) // 第二个参数:将父视图的padding值、child的margin值、父视图中已使用的尺寸 这三项进行相加 // 我们可以把它理解为除MyCustomTextViewA 的本身尺寸之外所有空间的总和 // 第三个参数:child的width值(childHeightMeasureSpec同理) // 共同计算出childWidthMeasureSpec的值 // 所以,getChildMeasureSpec这个方法的作用,就是根据 // 1、父视图MyCustomLinearLayoutA的MeasureSpec // 2、除MyCustomTextViewA 的本身尺寸之外所有空间的总和 // 3、MyCustomTextViewA 本身的尺寸 // 这三个方面共同来计算出MyCustomTextViewA 的MeasureSpec // 来看具体的逻辑: public static intgetChildMeasureSpec(int spec, int padding, int childDimension) { // 得到父视图的mode和size int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us // 由于本demo中,MyCustomTextViewA父视图的specMode为EXACTLY,所以,只看一条case语句了: case MeasureSpec.EXACTLY: // 在case语句中判断完父视图MyCustomLinearLayoutA的mode之后,再来判断MyCustomTextViewA的尺寸 // 也就是上文通过布局参数拿到的 我们在布局文件里写的宽和高的值 if (childDimension >= 0) { // 如果我们在布局文件中为MyCustomTextViewA 设置了具体的宽和高的值,那么将会用这个具体的值和EXACTLY // 组合成一个MeasureSpec并返回 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. // 如果我们在布局文件中为MyCustomTextViewA 设置的宽和高是MATCH_PARENT,那么将会用上文计算出的 // 可用空间(在本demo中为父视图的宽度和高度)和EXACTLY组合成一个MeasureSpec并返回 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // 如果我们在布局文件中为MyCustomTextViewA 设置的宽和高是WRAP_CONTENT,那么将会用上文计算出的 // 可用空间(在本demo中为父视图的宽度和高度)和AT_MOST组合成一个MeasureSpec并返回 // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } // 通过以上分析,MyCustomTextViewA的onMeasure方法接收的参数对应的size不一定是其父视图的宽高 // 可以在布局文件中,将MyCustomTextViewA的layout_width 改为"wrap_content"或具体的一个dp值 // 再复写MyCustomTextViewA的onMeasure方法,进行验证 break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } // 在这里,将以上文得到的childWidthMeasureSpec和childHeightMeasureSpec作为参数,执行child.measure方法 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // 对于ViewGroup类型的视图来讲,接着遍历,执行上文的操作 // 对于非ViewGroup类型的MyCustomTextViewA来讲,则是调用view类的onMeasure方法 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { // 简单来讲就是将计算得到的具体尺寸赋给成员变量mMeasuredWidth和mMeasuredHeight mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; } } } } } // for循环结束 } 以上就是view的measure的大致流程了 简单来说measure过程的作用就是: 在Activity 的setContentView() 方法执行后,从应用程序窗口的根view开始,根据屏幕分辨率、我们选择的窗口布局(是否全屏等)、xml文件中的layout_width、layout_height(可能为具体的值,也可能为match_parent或者wrap_content)、layout_weight、margin和padding的值进行递归操作,计算出每一个view的宽和高,并设置给成员变量mMeasuredWidth 和mMeasuredHeight 的过程。
所以,我们自定义view对象,重写onMeasure方法的目的就是按照我们的方式计算并设置view对象的宽和高,比如: @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(201, 801); } 这样的话就把View默认的measure流程覆盖掉了,不管在布局文件中定义的大小是多少,最终在界面上显示的大小都将会是201*801. 可是,上述代码的意义是什么呢?或者说在什么情况下我们需要重写onMeasure方法呢? 在不想要遵循view的默认measure流程时,或者需要动态调整布局,不太容易在xml文件中操作时, 我们可以考虑重写view的onMeasure方法。
在上文中,我们提到,view的measure过程使用到了xml文件中的layout_width、layout_height、layout_weight、margin和padding这些属性,那view的绘制过程的第二步layout和第三步draw是否会使用到xml文件中的其他属性呢?回答是肯定的。
虽然measure翻译为测量,layout翻译为布局,但其实这两个步骤都是在计算,只不过measure过程是在计算view的大小,layout过程是在计算view的坐标而已。view绘制的第二步 — layout ,核心就是根据xml文件中的gravity属性和第一步 — measure所得到的宽和高的值计算出view对象的坐标。大小和位置都确定了,就可以进行第三步 —draw了。
更多相关文章
- android隐藏以及显示软键盘以及不自动弹出键盘的方法
- Android(安卓)Keep screen on(保持屏幕唤醒)
- Android获取屏幕宽高的方法
- Android(安卓)平板电脑的判断方法
- Android(安卓)RectF类的构造函数参数说明
- 浅谈Java中Collections.sort对List排序的两种方法
- mybatisplus的坑 insert标签insert into select无参数问题的解决
- Python技巧匿名函数、回调函数和高阶函数
- Python list sort方法的具体使用