该博文所用的demo结构图: 对应的代码: MainActivity.java:
<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.widthlp.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(); // 然后,计算childWidthMeasureSpecchildHeightMeasureSpec的值 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、父视图MyCustomLinearLayoutAMeasureSpec // 2、MyCustomTextViewA 的本身尺寸之外所有空间的总和 // 3、MyCustomTextViewA 本身尺寸 // 这三个方面共同来计算出MyCustomTextViewAMeasureSpec // 来看具体的逻辑: 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不一定是其父视图的宽高 // 可以在布局文件中,将MyCustomTextViewAlayout_width 改为"wrap_content"或具体的一个dp值 // 再复写MyCustomTextViewA的onMeasure方法,进行验证 break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } // 在这里,将以上文得到的childWidthMeasureSpecchildHeightMeasureSpec作为参数,执行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了。


更多相关文章

  1. android隐藏以及显示软键盘以及不自动弹出键盘的方法
  2. Android(安卓)Keep screen on(保持屏幕唤醒)
  3. Android获取屏幕宽高的方法
  4. Android(安卓)平板电脑的判断方法
  5. Android(安卓)RectF类的构造函数参数说明
  6. 浅谈Java中Collections.sort对List排序的两种方法
  7. mybatisplus的坑 insert标签insert into select无参数问题的解决
  8. Python技巧匿名函数、回调函数和高阶函数
  9. Python list sort方法的具体使用

随机推荐

  1. Android(安卓)端 博客园闪存——alpha版
  2. Android(安卓)adb不是内部或外部命令 问
  3. Android图片加载神器之Fresco,基于各种使
  4. android 简历 android 3年 上海.doc
  5. 从Android中Activity之间的通信说开来
  6. 几行代码看程序员的水平——Android文件
  7. android ListView 几个重要属性
  8. Android(安卓)高手进阶教程(十四)之----A
  9. Android的Task和Activity相关
  10. 重新审视 Android