Android开发自定义View实战之自定义流式布局

目录

实现的效果

知识储备

MeasureSpec

代码讲解

第一步测量:onMeasure

部分代码说明

第一步放置:onLayout

完整代码

使用方法


实现的效果

知识储备

MeasureSpec

MeasureSpec是View的一个内部类,也是一个工具类,它承载了父控件对子控件的布局期望,能够替我们去生成测量规则。是一个int型变量,前2位代表测量模式SpecMode,后30位代表测量尺寸SpecSize

共有以下三种模式:

SpecMode说明
UNSPECIFIED父容器不对View有任何限制,一般用于系统内部,子View可以得到自己想要的任意大小
EXACTLY父容器已经检测出View所需大小,View的最终大小为SpecSize。对应LayoutParams中的match_parent或具体数值。
AT_MOST父容器指定了一个可用大小SpecSizeView的大小不能超过这个阈值。对应LayoutParams中的wrap_content

UNSPECIFIED我们在自定义view里面基本上不使用,而如果我们给控件设置为match_parent或者给出一个具体的数值比如说100dp则它的SpecMode为UNSPECIFIED。设置为wrap_content则它的SpecMode为AT_MOST。

代码讲解

第一步测量:onMeasure

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //得到自己的测量模式
        //测量规则MeasureSpec 1.测量模式SpecMode 2.测量大小SpecSize
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //根据所有子控件的宽高来决定自己的宽高
        int childCountWidth = 0;
        int childCountHeight = 0;
        //一行中最高的子控件的高度,同时这个变量也是这行的高度
        int lineMaxHeight = 0;
        //一行中已经被使用掉的宽度
        int lineCountWidth = 0;
        //后面会对子控件进行遍历,这两个变量存储当前遍历到的子控件的宽高
        int iChildWidth = 0;
        int iChildHeight = 0;
        //子控件是数量
        int childCount = getChildCount();
        //保存每一行的控件
        List<View> lineViewList = new ArrayList<>();
        if (!isMeasure) {
            isMeasure = true;
        } else {
            for (int i = 0; i < childCount; i++) {
                //获取子控件
                View childView = getChildAt(i);
                //测量子控件
                measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
                //得到当前控件的宽高
                iChildWidth = childView.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
                iChildHeight = childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
                //通过计算得出是否需要换行
                if (lineCountWidth + iChildWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
                    //需要换行
                    //对比每一行的宽度,最宽的作为流式布局的宽度
                    childCountWidth = Math.max(childCountWidth, lineCountWidth);
                    //累加得出父控件的总高度
                    childCountHeight += lineMaxHeight;
                    //把每一行的高记录起来
                    listLineHeight.add(lineMaxHeight);
                    //把每一行的子控件的集合记录起来
                    totalLineViewList.add(lineViewList);
                    //因为需要换行了,而lineViewList保存的是每一行的子控件,所有要重新new
                    lineViewList = new ArrayList<>();
                    //把这个需要换行的子控件加入到新的一行里面
                    lineViewList.add(childView);
                    //把上一行记录的数据重新设置
                    //lineCountWidth变量记录的是当前这一行的已经使用的宽度,所以直接等于这个子控件的宽度iChildWidth
                    lineCountWidth = iChildWidth;
                    lineMaxHeight = iChildHeight;
                } else {
                    //不需要换行
                    //更新一行中已经使用掉的宽度
                    lineCountWidth += iChildWidth;
                    //对比得出这一行的最高值
                    lineMaxHeight = Math.max(lineMaxHeight, iChildHeight);
                    //把一行的所有控件保存起来
                    lineViewList.add(childView);
                }
                if (i == childCount - 1) {
                    //需要换行
                    //对比每一行的宽度,最宽的作为流式布局的宽度
                    childCountWidth = Math.max(childCountWidth, lineCountWidth);
                    //累加得出父控件的总高度
                    childCountHeight += lineMaxHeight;
                    //把每一行的高记录起来
                    listLineHeight.add(lineMaxHeight);
                    //把每一行的子控件的集合记录起来
                    totalLineViewList.add(lineViewList);
                }
            }
        }

        int measureWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : childCountWidth;
        int measureHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : childCountHeight;
        //来设置自己最终的大小
        setMeasuredDimension(measureWidth, measureHeight);
    }
部分代码说明

上面的代码注释已经很详细了,这里对几个几个可能会疑惑的地方说明一下:

1.这里使用了isMeasure变量的目的是为了保证我们的测量方法只会执行一次,如果说我们的测量方法执行多次,那最后的效果会显示不出来。

 if (!isMeasure) {
            isMeasure = true;
        } else {

2.在执行完判断是否需要换行的代码之后我们后面添加了一段这样的代码,如果没有下面这段代码,那么我们流式布局中最后一行数据会显示不出来,比如说我们测量完一共有5行,则最后效果只显示了4行。

  if (i == childCount - 1) {
                    //需要换行
                    //对比每一行的宽度,最宽的作为流式布局的宽度
                    childCountWidth = Math.max(childCountWidth, lineCountWidth);
                    //累加得出父控件的总高度
                    childCountHeight += lineMaxHeight;
                    //把每一行的高记录起来
                    listLineHeight.add(lineMaxHeight);
                    //把每一行的子控件的集合记录起来
                    totalLineViewList.add(lineViewList);
                }
                

为什么会导致这样的后果我们再来梳理一遍换行部分的逻辑代码

if (lineCountWidth + iChildWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
                    //需要换行
                    //对比每一行的宽度,最宽的作为流式布局的宽度
                    childCountWidth = Math.max(childCountWidth, lineCountWidth);
                    //累加得出父控件的总高度
                    childCountHeight += lineMaxHeight;
                    //把每一行的高记录起来
                    listLineHeight.add(lineMaxHeight);
                    //把每一行的子控件的集合记录起来
                    totalLineViewList.add(lineViewList);
                    //因为需要换行了,而lineViewList保存的是每一行的子控件,所有要重新new
                    lineViewList = new ArrayList<>();
                    //把这个需要换行的子控件加入到新的一行里面
                    lineViewList.add(childView);
                    //把上一行记录的数据重新设置
                    //lineCountWidth变量记录的是当前这一行的已经使用的宽度,所以直接等于这个子控件的宽度iChildWidth
                    lineCountWidth = iChildWidth;
                    lineMaxHeight = iChildHeight;
                } else {
                    //不需要换行
                    //更新一行中已经使用掉的宽度
                    lineCountWidth += iChildWidth;
                    //对比得出这一行的最高值
                    lineMaxHeight = Math.max(lineMaxHeight, iChildHeight);
                    //把一行的所有控件保存起来
                    lineViewList.add(childView);
                }

当我们需要换行的时候,我们会把上一行的数据保存在我们的总集合里面,可是如果现在是最后一行,它是不会执行到需要换行那部分代码里面的,因为它都是最后一行了,怎么还会需要换行呢。也就是说,最后一行永远不会成为上一行,那它这行的数据就不会被添加到totalLineViewList里面去,相当于这行被丢掉了。所以我们 if (i == childCount - 1)这个条件来判断当前判断的子控件是否为父控件的最后一个子控件,是的话就把这行view的集合添加到我们的totalLineViewList里面去(最后一个子控件一定是最后一行)。

//把每一行的子控件的集合记录起来
totalLineViewList.add(lineViewList);

3.MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();

这行代码直接使用会报错,需要我们重写一下generateLayoutParams(),目的是我们要的MarginLayoutParams会包含子空间的Margin值,而原来的LayoutParams不包含。

 @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

4.我们最后用了这样一段代码来确定父控件的宽度和高度,其实就是判断一下父控件的SpecMode是否为MeasureSpec.EXACTLY,我们文章开头也说过,如果控件的SpecMode是否为MeasureSpec.EXACTLY说明它应该是被设置了match_parent或者是设置的具体的数值,所以这个时候直接将widthSize作为流式布局的长度宽度即可,如果不是这种情况,则说明父控件是设置了wrap_content,所以要将测量了所有子控件后得出的长度宽度给父控件,也就是我们的childCountWidth,childCountHeight。

int measureWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : childCountWidth;
        int measureHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : childCountHeight;
        //来设置自己最终的大小
        setMeasuredDimension(measureWidth, measureHeight);

第一步放置:onLayout

我们摆放的时候是根据四个坐标来确认摆放的位置的,所以代码逻辑就是通过去计算得出子控件的top, left, right, bottom来摆放

 //摆放
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        //摆放子控件的位置
        int top, left, right, bottom;
        //得到父控件的Padding值
        int countTop = getPaddingTop();
        int countLeft = getPaddingLeft();
        for (List<View> list : totalLineViewList) {
            for (View view : list) {
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams(); 
                top = countTop + marginLayoutParams.topMargin;
                left = countLeft + marginLayoutParams.leftMargin;
                right = left + view.getMeasuredWidth();
                if (right > r) {
                    right = r - marginLayoutParams.rightMargin;
                }
                bottom = top + view.getMeasuredHeight();
                view.layout(left, top, right, bottom);
                countLeft += view.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
            }
            //获取当前的行数
            int position = totalLineViewList.indexOf(list);
            //listLineHeight记录是每一行的行高,当摆放完一行要摆放下一行时,应该将本行的行高加上
            countTop += listLineHeight.get(position);
            countLeft = getPaddingLeft();
        }
        //把list清空
        totalLineViewList.clear();
        listLineHeight.clear();
    }

完整代码


import android.content.Context;
import android.icu.util.Measure;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

public class FlowlayoutView extends ViewGroup {
    public FlowlayoutView(Context context) {
        super(context);
    }

    public FlowlayoutView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowlayoutView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }


    List<List<View>> totalLineViewList = new ArrayList<>();
    List<Integer> listLineHeight = new ArrayList<>();
    private boolean isMeasure = false;

    /**
     * 第一步测量
     *
     * @param widthMeasureSpec  由父控件和自己共同决定
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //得到自己的测量模式
        //测量规则MeasureSpec 1.测量模式SpecMode 2.测量大小SpecSize
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //根据所有子控件的宽高来决定自己的宽高
        int childCountWidth = 0;
        int childCountHeight = 0;
        //一行中最高的子控件的高度,同时这个变量也是这行的高度
        int lineMaxHeight = 0;
        //一行中已经被使用掉的宽度
        int lineCountWidth = 0;
        //后面会对子控件进行遍历,这两个变量存储当前遍历到的子控件的宽高
        int iChildWidth = 0;
        int iChildHeight = 0;
        //子控件是数量
        int childCount = getChildCount();
        //保存每一行的控件
        List<View> lineViewList = new ArrayList<>();
        if (!isMeasure) {
            isMeasure = true;
        } else {
            for (int i = 0; i < childCount; i++) {
                //获取子控件
                View childView = getChildAt(i);
                //测量子控件
                measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
                //得到当前控件的宽高
                iChildWidth = childView.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
                iChildHeight = childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
                //通过计算得出是否需要换行
                if (lineCountWidth + iChildWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
                    //需要换行
                    //对比每一行的宽度,最宽的作为流式布局的宽度
                    childCountWidth = Math.max(childCountWidth, lineCountWidth);
                    //累加得出父控件的总高度
                    childCountHeight += lineMaxHeight;
                    //把每一行的高记录起来
                    listLineHeight.add(lineMaxHeight);
                    //把每一行的子控件的集合记录起来
                    totalLineViewList.add(lineViewList);
                    //因为需要换行了,而lineViewList保存的是每一行的子控件,所有要重新new
                    lineViewList = new ArrayList<>();
                    //把这个需要换行的子控件加入到新的一行里面
                    lineViewList.add(childView);
                    //把上一行记录的数据重新设置
                    //lineCountWidth变量记录的是当前这一行的已经使用的宽度,所以直接等于这个子控件的宽度iChildWidth
                    lineCountWidth = iChildWidth;
                    lineMaxHeight = iChildHeight;
                } else {
                    //不需要换行
                    //更新一行中已经使用掉的宽度
                    lineCountWidth += iChildWidth;
                    //对比得出这一行的最高值
                    lineMaxHeight = Math.max(lineMaxHeight, iChildHeight);
                    //把一行的所有控件保存起来
                    lineViewList.add(childView);
                }
                if (i == childCount - 1) {
                    //需要换行
                    //对比每一行的宽度,最宽的作为流式布局的宽度
                    childCountWidth = Math.max(childCountWidth, lineCountWidth);
                    //累加得出父控件的总高度
                    childCountHeight += lineMaxHeight;
                    //把每一行的高记录起来
                    listLineHeight.add(lineMaxHeight);
                    //把每一行的子控件的集合记录起来
                    totalLineViewList.add(lineViewList);
                }
            }
        }

        int measureWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : childCountWidth;
        int measureHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : childCountHeight;
        //来设置自己最终的大小
        setMeasuredDimension(measureWidth, measureHeight);
    }

    //摆放
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        //摆放子控件的位置
        int top, left, right, bottom;
        int countTop = getPaddingTop();
        int countLeft = getPaddingLeft();
        for (List<View> list : totalLineViewList) {
            for (View view : list) {
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
                top = countTop + marginLayoutParams.topMargin;
                left = countLeft + marginLayoutParams.leftMargin;
                right = left + view.getMeasuredWidth();
                if (right > r) {
                    right = r - marginLayoutParams.rightMargin;
                }
                bottom = top + view.getMeasuredHeight();
                view.layout(left, top, right, bottom);
                countLeft += view.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
            }
            //获取当前的行数
            int position = totalLineViewList.indexOf(list);
            countTop += listLineHeight.get(position);
            countLeft = getPaddingLeft();
        }
        totalLineViewList.clear();
        listLineHeight.clear();
    }


}

使用方法

子控件xml文件

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_wf"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="10dp"
    android:padding="8dp"
    android:textSize="12sp"></Button>

activity里面使用

这里的使用方法还是太繁琐了,可以在流式布局内部再封装一下,具体看自己的需求,这里就不展示了

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FlowlayoutView flowlayoutView=findViewById(R.id.flv);
        List<String> list=new ArrayList<>();
        list.add("竞争对手卡SZF成都市");
        list.add("安装国v就的饭局");
        list.add("不能形成");
        list.add("是的苏东坡匹配【】欧派认识的啊");
        list.add("范德萨");
        list.add("啊法国人");
        list.add("仨人规定骄傲");
        list.add("奥i热点视频");
        list.add("啊热哦怕");
        list.add("啊十九日公开lads南方国家在大润发");
        for (String s:list){
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            TextView textView =(TextView) layoutInflater.inflate(
                    R.layout.flowlayout_button,
                    flowlayoutView,
                    false
            );
            textView.setText(s);
            flowlayoutView.addView(textView);
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值