Android高级UI开发(三十六)measure测量实例
今天我们来通过一个实例来讲解一下view的测量绘制过程。我们打算做一个瀑布流标签,就是各个标签的宽高都不一样,当一行占满时就自动换行,示意图如下:
,这个与Grid是有区别的,Grid是每行有固定的列数,而我们这个瀑布流标签是一行占满为止才换行。我们打算做一个自定义容器,这个容器内部的各个子控件会自动按瀑布流标签的形式摆放,对各个子控件的类型没有要求。这样的话,我们创建一个类FlowLayout,让它继承ViewGroup,然后重写onMeasure函数,onLayout函数,至于为何不重写onDraw函数,我们实现了功能后再作分析。
接下来我们先分析FlowLayout,它的源代码如下:
-
package com.test.measure;
-
-
import android.content.Context;
-
import android.util.AttributeSet;
-
import android.util.DisplayMetrics;
-
import android.util.Log;
-
import android.view.View;
-
import android.view.ViewGroup;
-
-
import java.util.ArrayList;
-
import java.util.List;
-
-
/**
-
* Created by xiaowei on 2019/12/5.
-
*/
-
-
public class FlowLayout extends ViewGroup {
-
private static final String TAG = "FlowLayout" ;
-
/**
-
* 用来保存每行views的列表
-
*/
-
private List<List<View>> mViewLinesList = new ArrayList<>();
-
/**
-
* 用来保存行高的列表
-
*/
-
private List<Integer> mLineHeights = new ArrayList<>();
-
-
public FlowLayout(Context context, AttributeSet attrs) {
-
super(context, attrs);
-
}
-
-
@Override
-
public LayoutParams generateLayoutParams(AttributeSet attrs) {
-
return new MarginLayoutParams(getContext(),attrs);
-
}
-
-
@Override
-
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
mViewLinesList.clear();
-
mLineHeights.clear();
-
-
// 获取父容器为FlowLayout设置的测量模式和大小
-
int iWidthMode = MeasureSpec.getMode(widthMeasureSpec); //EXACT
-
int iHeightMode = MeasureSpec.getMode(heightMeasureSpec); //AT_MOST
-
int iWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec); //1080,EXACT
-
int iHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec); //1950(有底部虚拟导航),2076(虚拟导航不可见),AT_MOST
-
-
-
int measuredWith = 0; //这个自定义容器的测量宽度
-
int measuredHeight = 0;//这个自定义容器的测量高度
-
int iCurLineW = 0; //当前行宽,不断累加,超过屏幕宽度,则转向第二行
-
int iCurLineH = 0; //当前行高
-
-
if(iWidthMode == MeasureSpec.AT_MOST )
-
{
-
Log.i(TAG,"widthMode=AT_MOST,iWidthSpecSize="+iWidthSpecSize);
-
}else if(iWidthMode == MeasureSpec.UNSPECIFIED)
-
{
-
Log.i(TAG,"widthMmode=UNSPECIFIED,iWidthSpecSize="+iWidthSpecSize);
-
-
}else if(iWidthMode == MeasureSpec.EXACTLY)
-
{ Log.i(TAG,"widthMode=EXACTLY,iWidthSpecSize="+iWidthSpecSize);
-
-
}
-
-
if(iHeightMode == MeasureSpec.AT_MOST )
-
{
-
Log.i(TAG,"heightMode=AT_MOST,heightSize="+iHeightSpecSize);
-
}else if(iHeightMode == MeasureSpec.UNSPECIFIED)
-
{
-
Log.i(TAG,"heightMode=UNSPECIFIED,heightSize="+iHeightSpecSize);
-
}else if(iHeightMode == MeasureSpec.EXACTLY)
-
{
-
Log.i(TAG,"heightMode=EXACTLY,heightSize="+iHeightSpecSize);
-
}
-
-
if(iWidthMode == MeasureSpec.EXACTLY && iHeightMode == MeasureSpec.EXACTLY){
-
measuredWith = iWidthSpecSize;
-
measuredHeight = iHeightSpecSize;
-
}else{
-
int iChildWidth;
-
int iChildHeight;
-
int childCount = getChildCount();
-
List<View> viewList = new ArrayList<>();
-
for(int i = 0 ; i < childCount ; i++){
-
View childView = getChildAt(i);
-
measureChild(childView, widthMeasureSpec,heightMeasureSpec);
-
MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
-
iChildWidth = childView.getMeasuredWidth() + layoutParams.leftMargin +
-
layoutParams.rightMargin;
-
iChildHeight = childView.getMeasuredHeight() + layoutParams.topMargin +
-
layoutParams.bottomMargin;
-
-
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); //记录某一行的子控件,将来是二维数组的列
-
}
-
-
/*****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);
-
-
}
-
}
-
-
-
}
-
// 最终目的
-
setMeasuredDimension(measuredWith,measuredHeight);//实际ViewGroup的宽高:measuredWith=1072,measuredHeight=397
-
-
}
-
-
@Override
-
protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
int left,top,right,bottom;
-
int curTop = 0;
-
int curLeft = 0;
-
int lineCount = mViewLinesList.size();
-
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);
-
}
-
mViewLinesList.clear();
-
mLineHeights.clear();
-
}
-
-
-
-
-
}
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 猜想与实际测量模式与大小的对比
我们对比一下打印出的日志,发现与我们猜想的一样,日志如下:
-
FlowLayout: widthMode=EXACTLY,iWidthSpecSize=1080
-
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
-
View childView = getChildAt(i);
-
measureChild(childView, widthMeasureSpec,heightMeasureSpec);
-
MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
-
iChildWidth = childView.getMeasuredWidth() + layoutParams.leftMargin +
-
layoutParams.rightMargin;
-
iChildHeight = childView.getMeasuredHeight() + layoutParams.topMargin +
-
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
- 点赞
- 收藏
- 关注作者
评论(0)