Android 自定义流式布局FlowLayout 自己造的轮子真香!

效果图


二话不说,先上效果图
在这里插入图片描述

功能


1.支持设置行间距、列间距
2.支持适配器模式动态添加

实现分析


测量

在onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法中进行测量工作,主要是对自定义ViewGroup 的宽高进行测量。这里需要引入几个变量:行高、行宽以及期望的总高度。

行宽:当前行的宽度,不能超过GroupView 所能提供的最大宽度。当要添加一个子view 时,需先进行计算,如果添加的行宽未超过GroupView 所能提供的最大宽度,则追加到当前行宽。如果添加后的行宽大于最大宽度,则不能添加到该行,需要另起一行,而新一行的初始行宽即为当前子view 的测量宽度。

行高:用于记录行的高度,值为当前行中最高的子view 的高度。当未发生换行时,不断和新的子view高度进行对比,取最大高度。当发生换行时,则需要把行高置为换行后第一个显示的子view 的高度。

期望的总高度:简单来说就是当高度设置为wrap_content 时,测量的高度,其计算公式为:ViewGroup的上下内间距 + 行高…+行高。当添加子view 后如果引起换行,那么就追加该行的高度到总高度上 。

伪代码

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    for (遍历子view) {
        测量子view 的宽高
        if (行宽 + 子view的宽度 + 左右内边距 <= 当前能提供的最大宽度){// 添加子view后不需要换行
            行宽 = 行宽 + 子view的宽度
            行高 = 行高和子view 高度中的最大值
    
        } else { // 添加子view后需要换行
            // 更新最新一行的宽度为此child 的测量宽度
            行宽 = 子view 的宽度;
            // 期望高度追加当前行的行高。
            期望的总高度 = 期望的总高度 + 行高
        }
    }
    // 这里添加的是最后一行的高度。因为上面是在换行时才追加的行高,
    // 在不需要换行时并没有追加行高,丢失了不满足换行条件下的行高。
    // 举例说明:比如一行最多显示5个,但是当前只有1个,或者当前有6个的情况下,少了一行的行高。
    期望的总高度 = 期望的总高度 + 行高;
     
    // 期望的总高度追加内外边距
    期望的总高度 = 期望的总高度 + 上下内边距
    
    存储计算得到的宽高,即调用 android.view.View.setMeasuredDimension() 方法
}

布局

在onLayout(boolean changed, int l, int t, int r, int b)方法中进行布局工作,计算子view 的起始位置,即顶部偏移量和左侧偏移量,待计算得出偏移量之后调用子view 的 android.view.View#layout 方法对其进行布局。这里需要引入几个变量:child的顶部偏移、child的左侧偏移、以及行高。

行高:用于记录当前行的最高高度,在需要换行时,追加该行高到顶部偏移量上,这样也就得出最新一行的顶部偏移距离了,同时更新最新的行高初始值为新的子view 高度。

child的顶部偏移:用于布局子view 的top 位置,同一行的子view 拥有相同的顶部偏移距离。在布局过程中,不断记录需要布局的子view 的宽高,如果布局后宽度超过ViewGroup 提供的最大宽度,则更新顶部偏移为追加行高后的值,即 顶部偏移 = 当前偏移量+当前行高。

child的左侧偏移:用于布局子view 的left 位置。当新布局的子view 布局后,如果宽度未超过ViewGroup 提供的最大宽度,则使用当前数据进行子view布局。如果宽度超过最大宽度,则换行显示,换行后的左侧偏移为初始值,而后进行子view 的布局工作。待改子view布局完成后,更新左侧偏移为下一个需要布局的子view的左侧偏移距离,即 下一个子view 的左侧偏移距离 = 当前左侧偏移 + 当前子view 的测量宽度。

伪代码

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (遍历子view) {
        测量子view 的宽高

        if (左偏移量 + 子view的宽度 + 内边距 <= 支持的最大宽度) {// 该child加入后未超过一行宽度。
            // 行高取这一行中最高的child height
            行高 =  当前行高与新子view 高度中的最大值

        } else {// 超过一行,换行显示。换行后的左侧偏移为初始值,顶部偏移为当前偏移量+当前行高
            左偏移距离 = 初始化数据
            上偏移距离 = 上偏移距离 + 行高
            行高 = 子view 的测量高度
        }
        
        // 子view 布局
        child.layout(,,,);

        // 更新左侧偏移距离,即下一个child 的left
        左偏移距离 = 左偏移距离 + 当前子view 的测量宽度;
    }
}

间距实现

其实间距问题挺容易处理,无非就是在测量和布局的时候额外额外考虑一个值就好了。

行间距

首先来看行间距,无论是在测量还是在布局过程中,只有在发生换行的时候才需要考虑行间距。在测量过程中,当需要换行时,需要把行间距追加到期望的总高度中。而在布局过程中,当需要换行时,需要把行间距追加到顶部偏移量上就好了,这样也就表示了下一行的顶部位置中包含了行间距。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
        if (行宽 + 子view的宽度 + 左右内边距 <= 当前能提供的最大宽度){// 添加子view后不需要换行
           ......
    
        } else { // 添加子view后需要换行
            ......
            // 期望高度追加当前行的行高、行间距
            期望的总高度 = 期望的总高度 + 行高 + 行间距
        }
        ......
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ......
        if (左偏移量 + 子view的宽度 + 内边距 <= 支持的最大宽度) {// 该child 加入后未超过一行宽度。
            ......
        } else {// 超过一行,换行显示。换行后的左侧偏移为初始值,顶部偏移为当前偏移量+当前行高
            ......
            上偏移距离 = 上偏移距离 + 行高 + 行间距
            ......
        }
       ......
} 
列间距

其次来看下列间距,首先第一个子child 是不需要有列间距的,而其后的子view在进行测量时需要考虑到间距问题,在进行行宽计算的时候需要加上列间距,当不满足换行条件时,在更新行宽的时候除了要追加子view的测量宽度之外,还要额外添加对应的列间距。而在布局过程中,在当前子view布局完成之后,更新下一个子view的左侧偏移量时同样要把列间距计入在内。因为在换行的时候把左侧偏移量进行了初始化,所以并不会对最左侧子view产生影响。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    for (遍历子view) {
        ......
        列间距 = 如果是第一个子view,则为0,否则为正常的设定值
        if (行宽 + 子view的宽度 + 左右内边距 + 列间距 <= 当前能提供的最大宽度){// 添加子view后不需要换行
            行宽 = 行宽 + 子view的宽度 + 列间距
            ......
        } else { // 添加子view后需要换行
           ......
        }
    }
    ......
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (遍历子view) {
        ......
        // 更新左侧偏移距离(+间距),即下一个child 的left
        左偏移距离 = 左偏移距离 + 当前子view 的测量宽度 + 列间距;
    }
}

适配器实现动态添加

可使用 android.widget.BaseAdapter 的实现类进行动态添加,当前仅简单实现view 的添加,后续进行迭代优化。

代码实现


/**
 * 描述 : 流式view
 * 作者 : shiguotao
 * 版本 : V1
 * 创建时间 : 2020/3/24 8:08 PM
 */
public class FlowLayout extends ViewGroup {

    private final String TAG = "FlowLayout";

    private float mRowSpacing = 0.0f;// 行间距
    private float mColumnSpacing = 0.0f;// 列间距

    public FlowLayout(Context context) {
        this(context, null);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);

        mRowSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_rowSpacing, 0);
        mColumnSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_columnSpacing, 0);
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int expectHeight = 0;// 期望高度,累加 child 的 height
        int lineWidth = 0;// 单行宽度,动态计算当前行的宽度。
        int lineHeight = 0;// 单行高度,取该行中高度最大的view
        float widthSpacing;

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 测量子view 的宽高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            widthSpacing = i == 0 ? 0 : mColumnSpacing;

            // 这里进行的是预判断。追加该child 后,行宽
            // 若未超过提供的最大宽度,则行宽需要追加child 的宽度,并且计算该行的最大高度。
            // 若超过提供的最大宽度,则需要追加该行的行高,并且更新下一行的行宽为当前child 的测量宽度。
            if (lineWidth + widthSpacing + childWidth + getPaddingLeft() + getPaddingRight() <= widthSize) {// 未超过一行
                // 追加行宽。
                lineWidth += widthSpacing + childWidth;
                // 不断对比,获取该行的最大高度
                lineHeight = Math.max(lineHeight, childHeight);

            } else {// 超过一行
                // 更新最新一行的宽度为此child 的测量宽度
                lineWidth = childWidth;
                // 期望高度追加当前行的行高。
                expectHeight += lineHeight + mRowSpacing;
            }
        }

        // 这里添加的是最后一行的高度。因为上面是在换行时才追加的行高,在不需要换行时并没有追加行高,丢失了不满足换行条件下的行高。
        // 举例说明:比如一行最多显示5个,但是当前只有1个,或者当前有6个的情况下,少了一行的行高。
        expectHeight += lineHeight;

        // 追加ViewGroup 的内边距
        expectHeight += getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(widthSize, resolveSize(expectHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int width = r - l;
        int childLeftOffset = getPaddingLeft();// child view 的left偏移距离,用于记录左边位置。
        int childTopOffset = getPaddingTop();// child view 的top偏移距离,用于记录顶部位置。

        int lineHeight = 0;// 行高

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            //跳过View.GONE的子View
            if (child.getVisibility() == View.GONE) {
                continue;
            }

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            if (childLeftOffset + childWidth + getPaddingRight() <= width) {// 该child 加入后未超过一行宽度。
                // 行高取这一行中最高的child height
                lineHeight = Math.max(lineHeight, childHeight);

            } else {// 超过一行,换行显示。换行后的左侧偏移为初始值,顶部偏移为当前偏移量+当前行高
                childLeftOffset = getPaddingLeft();
                childTopOffset += lineHeight + mRowSpacing;
                lineHeight = childHeight;
            }
            child.layout(childLeftOffset, childTopOffset, childLeftOffset + childWidth, childTopOffset + childHeight);

            // 更新左侧偏移距离(+间距),即下一个child 的left
            childLeftOffset += childWidth + mColumnSpacing;
        }
    }

    public void setAdapter(BaseAdapter mAdapter) {
        this.removeAllViews();
        for (int i = 0; i < mAdapter.getCount(); i++) {
            View view = mAdapter.getView(i, null, this);
            this.addView(view);
        }
        requestLayout();
    }

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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值