Android高级UI开发(三十六)measure测量实例

举报
yd_57386892 发表于 2020/12/28 22:44:30 2020/12/28
【摘要】           今天我们来通过一个实例来讲解一下view的测量绘制过程。我们打算做一个瀑布流标签,就是各个标签的宽高都不一样,当一行占满时就自动换行,示意图如下: ,这个与Grid是有区别的,Grid是每行有固定的列数,而我们这个瀑布流标签是一行占满为止才换行。我们打算做一个自定义容器,这个容器内部的各个子控件会自动按瀑布流标签的形式摆放,对各个子控件的类型没有要求...

          今天我们来通过一个实例来讲解一下view的测量绘制过程。我们打算做一个瀑布流标签,就是各个标签的宽高都不一样,当一行占满时就自动换行,示意图如下:

,这个与Grid是有区别的,Grid是每行有固定的列数,而我们这个瀑布流标签是一行占满为止才换行。我们打算做一个自定义容器,这个容器内部的各个子控件会自动按瀑布流标签的形式摆放,对各个子控件的类型没有要求。这样的话,我们创建一个类FlowLayout,让它继承ViewGroup,然后重写onMeasure函数,onLayout函数,至于为何不重写onDraw函数,我们实现了功能后再作分析。

 

接下来我们先分析FlowLayout,它的源代码如下:


  
  1. package com.test.measure;
  2. import android.content.Context;
  3. import android.util.AttributeSet;
  4. import android.util.DisplayMetrics;
  5. import android.util.Log;
  6. import android.view.View;
  7. import android.view.ViewGroup;
  8. import java.util.ArrayList;
  9. import java.util.List;
  10. /**
  11. * Created by xiaowei on 2019/12/5.
  12. */
  13. public class FlowLayout extends ViewGroup {
  14. private static final String TAG = "FlowLayout" ;
  15. /**
  16. * 用来保存每行views的列表
  17. */
  18. private List<List<View>> mViewLinesList = new ArrayList<>();
  19. /**
  20. * 用来保存行高的列表
  21. */
  22. private List<Integer> mLineHeights = new ArrayList<>();
  23. public FlowLayout(Context context, AttributeSet attrs) {
  24. super(context, attrs);
  25. }
  26. @Override
  27. public LayoutParams generateLayoutParams(AttributeSet attrs) {
  28. return new MarginLayoutParams(getContext(),attrs);
  29. }
  30. @Override
  31. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  32. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  33. mViewLinesList.clear();
  34. mLineHeights.clear();
  35. // 获取父容器为FlowLayout设置的测量模式和大小
  36. int iWidthMode = MeasureSpec.getMode(widthMeasureSpec); //EXACT
  37. int iHeightMode = MeasureSpec.getMode(heightMeasureSpec); //AT_MOST
  38. int iWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec); //1080,EXACT
  39. int iHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec); //1950(有底部虚拟导航),2076(虚拟导航不可见),AT_MOST
  40. int measuredWith = 0; //这个自定义容器的测量宽度
  41. int measuredHeight = 0;//这个自定义容器的测量高度
  42. int iCurLineW = 0; //当前行宽,不断累加,超过屏幕宽度,则转向第二行
  43. int iCurLineH = 0; //当前行高
  44. if(iWidthMode == MeasureSpec.AT_MOST )
  45. {
  46. Log.i(TAG,"widthMode=AT_MOST,iWidthSpecSize="+iWidthSpecSize);
  47. }else if(iWidthMode == MeasureSpec.UNSPECIFIED)
  48. {
  49. Log.i(TAG,"widthMmode=UNSPECIFIED,iWidthSpecSize="+iWidthSpecSize);
  50. }else if(iWidthMode == MeasureSpec.EXACTLY)
  51. { Log.i(TAG,"widthMode=EXACTLY,iWidthSpecSize="+iWidthSpecSize);
  52. }
  53. if(iHeightMode == MeasureSpec.AT_MOST )
  54. {
  55. Log.i(TAG,"heightMode=AT_MOST,heightSize="+iHeightSpecSize);
  56. }else if(iHeightMode == MeasureSpec.UNSPECIFIED)
  57. {
  58. Log.i(TAG,"heightMode=UNSPECIFIED,heightSize="+iHeightSpecSize);
  59. }else if(iHeightMode == MeasureSpec.EXACTLY)
  60. {
  61. Log.i(TAG,"heightMode=EXACTLY,heightSize="+iHeightSpecSize);
  62. }
  63. if(iWidthMode == MeasureSpec.EXACTLY && iHeightMode == MeasureSpec.EXACTLY){
  64. measuredWith = iWidthSpecSize;
  65. measuredHeight = iHeightSpecSize;
  66. }else{
  67. int iChildWidth;
  68. int iChildHeight;
  69. int childCount = getChildCount();
  70. List<View> viewList = new ArrayList<>();
  71. for(int i = 0 ; i < childCount ; i++){
  72. View childView = getChildAt(i);
  73. measureChild(childView, widthMeasureSpec,heightMeasureSpec);
  74. MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
  75. iChildWidth = childView.getMeasuredWidth() + layoutParams.leftMargin +
  76. layoutParams.rightMargin;
  77. iChildHeight = childView.getMeasuredHeight() + layoutParams.topMargin +
  78. layoutParams.bottomMargin;
  79. if(iCurLineW + iChildWidth > iWidthSpecSize){
  80. //将要折行了,折行之前先记录当前行的宽度,如果当前行更宽一些,那么就把当前行的宽度给measuredWith(作为FlowLayout的宽度),iWidthSpecSize为FlowLayout的父容器LineareLayout给FlowLayout指定的精确宽度(EXACT模式),屏幕宽度1080
  81. /**1、记录当前行的信息***/
  82. //1、记录当前行的最大宽度,高度累加
  83. measuredWith = Math.max(measuredWith,iCurLineW);
  84. measuredHeight += iCurLineH;
  85. //2、将当前行的viewList添加至总的mViewsList,将行高添加至总的行高List
  86. mViewLinesList.add(viewList);//将当前行的控件组添加到“行二维数组”中
  87. mLineHeights.add(iCurLineH); //记录每一行的高度
  88. /**2、记录新一行的信息***/
  89. //1、重新赋值新一行的宽、高
  90. iCurLineW = iChildWidth;
  91. iCurLineH = iChildHeight;
  92. // 2、新建一行的viewlist,添加新一行的view
  93. viewList = new ArrayList<View>();
  94. viewList.add(childView);
  95. }else{
  96. // 记录某行内的消息
  97. //1、行内宽度的叠加、高度比较
  98. iCurLineW += iChildWidth;
  99. iCurLineH = Math.max(iCurLineH, iChildHeight);
  100. // 2、添加至当前行的viewList中
  101. viewList.add(childView); //记录某一行的子控件,将来是二维数组的列
  102. }
  103. /*****3、为最后一行 换行,否则measuredHeight就会少加最后一行,并且mViewLinesList也就少加最后一行控件**********/
  104. if(i == childCount - 1){
  105. //1、记录当前行的最大宽度,高度累加
  106. measuredWith = Math.max(measuredWith,iCurLineW);
  107. measuredHeight += iCurLineH;
  108. //2、将当前行的viewList添加至总的mViewsList,将行高添加至总的行高List
  109. mViewLinesList.add(viewList);
  110. mLineHeights.add(iCurLineH);
  111. }
  112. }
  113. }
  114. // 最终目的
  115. setMeasuredDimension(measuredWith,measuredHeight);//实际ViewGroup的宽高:measuredWith=1072,measuredHeight=397
  116. }
  117. @Override
  118. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  119. int left,top,right,bottom;
  120. int curTop = 0;
  121. int curLeft = 0;
  122. int lineCount = mViewLinesList.size();
  123. for(int i = 0 ; i < lineCount ; i++) {
  124. List<View> viewList = mViewLinesList.get(i);
  125. int lineViewSize = viewList.size();
  126. for(int j = 0; j < lineViewSize; j++){
  127. View childView = viewList.get(j);
  128. MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
  129. left = curLeft + layoutParams.leftMargin;
  130. top = curTop + layoutParams.topMargin;
  131. right = left + childView.getMeasuredWidth();
  132. bottom = top + childView.getMeasuredHeight();
  133. childView.layout(left,top,right,bottom);
  134. curLeft += childView.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
  135. }
  136. curLeft = 0;
  137. curTop += mLineHeights.get(i);
  138. }
  139. mViewLinesList.clear();
  140. mLineHeights.clear();
  141. }
  142. }

1. widthMeasureSpec与heightMeasureSpec的测量模式与大小


        我们首先看一下onMeasure函数,它的两个参数widthMeasureSpec和heightMeasureSpec,这2个参数是FlowLayout的父容器即Xml里的LinearLayout给FlowLayout的宽高规格。我们专门下了一段代码来打印日志,输出widthMeasureSpec的模式和大小,以及heightMeasureSpec的测量模式和大小。 

1.1  猜想测量模式与大小  

            我们可否根据XML布局文件,先猜测一下它们的测量模式和大小,xml布局如下:我们看到LinearLayout的  android:layout_width="match_parent",同时FlowLayout的android:layout_width="match_parent",LinearLayout的match_parent就是屏幕的宽度,这里是1080px,同时FlowLayout的layout_width匹配到LinearLayout的match_parent,也就是说FlowLayout的宽度和LinearLayout的宽度一样,那这样明显就是一个精确值1080,即LinearLayout给FlowLayout的测量宽度规格widthMeasureSpec,它的测量模式一定是EXACT,size是1080px。

            接下来我们看一下高度规格,LinearLayout的layout_height高度为match_parent,即整个屏幕的可用高度,这里是1950,本来应该是2000多像素,可能是把状态栏与虚拟导航栏除外了吧。而FlowLayout的layout_height为wrap_content,高度随它自己的内容高度而改变,这样的话,父容器LinearLayout有一个固定的高度match_parent(1950),子控件FlowLayout的内容又不是确定的,那就猜想子控件FlowLayout的高度至多不超过父容器LinearLayout的高度,即LinearLayout给的heightMeasureSpec高度规格的测量模式是AT_MOST, 同时size就是1950,这个规格意味着子控件FlowLayout的高度是动态变化的且最大不能超过1950px。

1.2  猜想与实际测量模式与大小的对比

我们对比一下打印出的日志,发现与我们猜想的一样,日志如下:


  
  1. FlowLayout: widthMode=EXACTLY,iWidthSpecSize=1080
  2. FlowLayout: heightMode=AT_MOST,heightSize=1876

细心的读者会发现size和我们上述1.1中的不一样,这是因为我换了一台手机,呵呵。

2. onMeasure函数深入浅出

好,接下来我们继续分析onMeasure代码,
     if(iWidthMode == MeasureSpec.EXACTLY && iHeightMode == MeasureSpec.EXACTLY){
            measuredWith = iWidthSpecSize;
            measuredHeight = iHeightSpecSize;
        }
显然我们现在的测量规格不会进入这个分支,这段代码表示如果父容器给当前FlowLayout宽、高测量模式都是精确值,那我们就不用再测量了,FlowLayout的宽高直接就是widthMeasureSpec和heightMeasureSpec中给定的大小,在这里再说一下测量规格是一个32位整数,高2位表示测试模式,低30位表示给定的大小值。那我们这里会进入for循环这段代码:
for(int i = 0 ; i < childCount ; i++){
......
}

在这个for循环里测量每一个子view(FlowLayout内的各个textview标签控件)的大小(宽,高),因为我们要知道当前父容器FlowLayout的测量大小,就得先知道各个子控件的宽高。FlowLayout的最终宽度肯定是各行控件中的最大行宽度。高度是多行控件累加的总高度,而每一行的高度应该取该行中高度最高的childView. 在这里我们就有一个疑问,我们FlowLayout的onMeasure传递进来的widthMeasureSpec规格不是已经是EXACT模式了吗?而且大小也给定了,即FlowLayout的宽度就是widthMeasureSpec中的大小1080px,那我们为什么还要根据一行child的宽度来再算一次FlowLayout的宽度呢,我现在只能说这样更精确一些,算下来FlowLayout的最终测量宽度是1072px.和widthMeasureSpec规格中给定的size差不多。那么如何来计算呢?

2. 1 先测量childView

第一步先测量childview


  
  1. View childView = getChildAt(i);
  2. measureChild(childView, widthMeasureSpec,heightMeasureSpec);
  3. MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
  4. iChildWidth = childView.getMeasuredWidth() + layoutParams.leftMargin +
  5. layoutParams.rightMargin;
  6. iChildHeight = childView.getMeasuredHeight() + layoutParams.topMargin +
  7. layoutParams.bottomMargin;


这段代码中的measureChild函数测量出了子childview的可视宽、高(不包括margin,也就是只包含内容+padding),iChildWidth 和iChildHeight 是将childview的margin属性考虑进去了,我们容器FlowLayout要容纳这些childview肯定要给这些margin留有空间。

2.2 计算出FlowLayout的宽、高

第二步,计算出行中的最大行宽,与各行的累加高度。

   if(iCurLineW + iChildWidth > iWidthSpecSize){
                    //将要折行了,折行之前先记录当前行的宽度,如果当前行更宽一些,那么就把当前行的宽度给measuredWith(作为FlowLayout的宽度),iWidthSpecSize为FlowLayout的父容器LineareLayout给FlowLayout指定的精确宽度(EXACT模式),屏幕宽度1080


                    /**1、记录当前行的信息***/

                    //1、记录当前行的最大宽度,高度累加
                    measuredWith = Math.max(measuredWith,iCurLineW);
                    measuredHeight += iCurLineH;
                    //2、将当前行的viewList添加至总的mViewsList,将行高添加至总的行高List
                    mViewLinesList.add(viewList);//将当前行的控件组添加到“行二维数组”中
                    mLineHeights.add(iCurLineH); //记录每一行的高度

                    /**2、记录新一行的信息***/

                    //1、重新赋值新一行的宽、高
                    iCurLineW = iChildWidth;
                    iCurLineH = iChildHeight;

                    // 2、新建一行的viewlist,添加新一行的view
                    viewList = new ArrayList<View>();
                    viewList.add(childView);

                }else{
                    // 记录某行内的消息
                    //1、行内宽度的叠加、高度比较
                    iCurLineW += iChildWidth;
                    iCurLineH = Math.max(iCurLineH, iChildHeight);

                    // 2、添加至当前行的viewList中
                    viewList.add(childView);  //记录某一行的子控件,将来是二维数组的列
                }

iCurLine0W 用于记录每一行的宽度,初始值为0,假设刚开始摆了一个Textview,iCurLine0W 的值就会变成iCurLineW +=iChildWidth; 也就是先进入else这个分子,iCurLineH 就是iChildHeight。当摆放第二个Textview,的时候iCurLine0W 继续累加第二个Textview的iChildWidth:iCurLine0W +=iChildWidth; 然后iCurLineH会和第二个 Textview的iChildHeight)对比,取最大值,总之当前行高iCurLineH的值总以这行中高度最高的Textview为准。然后,继续摆第三个Textview,假设这时的行宽累加:iCurLineW += iChildWidth将要超过FlowLayout的宽度iWidthSpecSize,那么就要换行显示,即进入if分支。换行前,先把这第一行的行宽iCurLineW作为measuredWith(FlowLayout的宽):  measuredWith = Math.max(measuredWith,iCurLineW); 这里会和后续行的行宽比较大小,总之measuredWith取所有行中最宽的一个值。同时高度measuredHeight也要累加一行的高度iCurLineH,即:measuredHeight += iCurLineH。FlowLayout的测量高度measuredHeight最终是各行高度之和。   mViewLinesList.add(viewList);//将当前行的控件组添加到“行二维数组”中
                    mLineHeights.add(iCurLineH); //记录每一行的高度。
这两行代码mViewLinesList二维数组,是为了保存每一行的子控件,包含多行。mLineHeights一维数组是保存每一行的高度。在这里保存是为了onLayout中摆放方便,在onLayout中会直接摆放mViewLinesList已归类的每一行控件,也就是说在onLayout中就没必要判断什么时候该折行了,因为每一行包含多少个控件,哪些控件该折行到下一行,mViewLinesList都已记录了。为onLayout省去了大量的代码,到时候直接摆放mViewLinesList里面的控件就行。

   //1、重新赋值新一行的宽、高
                    iCurLineW = iChildWidth;
                    iCurLineH = iChildHeight;

                    // 2、新建一行的viewlist,添加新一行的view
                    viewList = new ArrayList<View>();
                    viewList.add(childView);
这就是刚才上一行每有空间容纳的那个控件,它应该放在下一行,这时iCurLineW 下一行宽的初始值就是这个控件的宽。下一行的iCurLineH行高初始值 就是iChildHeight; 同时下一行控件数组的第一个元素就是当前childView.

    /*****3、为最后一行 换行,否则measuredHeight就会少加最后一行,并且mViewLinesList也就少加最后一行控件**********/
                if(i == childCount - 1){
                    //1、记录当前行的最大宽度,高度累加
                    measuredWith = Math.max(measuredWith,iCurLineW);
                    measuredHeight += iCurLineH;

                    //2、将当前行的viewList添加至总的mViewsList,将行高添加至总的行高List
                    mViewLinesList.add(viewList);
                    mLineHeights.add(iCurLineH);

                }

 if(i == childCount - 1)判断是最后一个孩子的话,强制触发换行,因为最后一行,比如只放了一个TextView的时候,它的宽度不足以触发上述if分支,也就是说measuredWith和measuredHeight就少计算一次,至少高度的累加上会少加这最后一行的高度:  measuredHeight += iCurLineH;

最终通过测量了所有child的大小,以及递归得到了FlowGroup的宽高(measuredWith,measuredHeight);
以下这行代码,用于将测量出的FlowGroup的宽高确定下来。将来绘制出来的FlowGroup占用的空间也就是这么大。

2.3  保存FlowLayout的大小


  setMeasuredDimension(measuredWith,measuredHeight);

3. onLayout摆放分析

核心代码如下:
    for(int i = 0 ; i < lineCount ; i++) {
            List<View> viewList = mViewLinesList.get(i);
            int lineViewSize = viewList.size();
            for(int j = 0; j < lineViewSize; j++){
                View childView = viewList.get(j);
                MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();

                left = curLeft + layoutParams.leftMargin;
                top = curTop + layoutParams.topMargin;
                right = left + childView.getMeasuredWidth();
                bottom = top + childView.getMeasuredHeight();
                childView.layout(left,top,right,bottom);
                curLeft += childView.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
            }
            curLeft = 0;
            curTop += mLineHeights.get(i);
        }

也就是for循环每一行childview,通过mViewLinesList.get(i)获得每一行的childview,然后根据每一个childview的宽、高、margin来 确定 childview的摆放位置,left,就是childview要摆放的左边缘,它肯定在leftMargin的右边, top,也一样考虑topMargin,childview的上边缘要在topMargin下。right,右边缘不需要考虑rightMarin。bottom,下边缘同样不用考虑bottomMargin .
最后,调用childView.layout(left,top,right,bottom)函数,传入left,top,right,bottom四个参数,实质就是一个矩形的左上角和右下角2个点的坐标,最终摆放了这个childView. 一行中第一个控件摆放完后,curLeft左边缘就要右移一个childview的宽(包含leftMargin和rightMargin),为摆放一行中的下一个childView做准备。当嵌套的for循环执行完后,就到了下一行的摆放,这是curLeft初始为0重新开始横向摆放,  curTop 就要下移一行的高度:  curTop += mLineHeights.get(i);

备注:  mViewLinesList.clear();
        mLineHeights.clear();  无论是onMeasure中的还是onLaout中的clear()都是为了避免重复调用onMeasure和onLaout导致数组内容不断重复增加的BUG.

好了,至此测量和摆放过程已分析完毕,这时候可以运行代码查看效果了。今天先到这里,没有鼠标,每一个字都很难敲上去。下一次我们来分析最后一个问题,为什么我们没有重写onDraw函数,也绘制出了这个瀑布流标签。

 

源码地址:https://download.csdn.net/download/gaoxiaoweiandy/12008467

文章来源: blog.csdn.net,作者:冉航--小虾米,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/gaoxiaoweiandy/article/details/103315173

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。