自定义ViewGroup--实现FlowLayout

自定义ViewGroup–实现FlowLayout

基本流程

0.自定义容器类继承ViewGroup

需要实现4个构造函数

public class FlowLayout extends ViewGroup {
    // 子View二维列表
    List<List<View>> allChild = new ArrayList<>();
    // 每行的高度,取每行的最大值
    List<Integer> itemHeightList = new ArrayList<>();
    // 子view占用父控件的空间,取最大行中的数据
    int usedWidth = 0;
    int usedHeight = 0;

    // 父容器内侧上下左右边距
    private int paddingLeft = 0;
    private int paddingRight = 0;
    private int paddingTop = 0;
    private int paddingBottom = 0;
    // 水平和垂直间距,通过FlowLayout的自定义属性来设置
    private int horizontalDivide = 0;
    private int verticalDivide = 0;
    
	// 在java代码中new使用
	public FlowLayout(@NonNull Context context)
	// 在xml中声明时使用
	public FlowLayout(@NonNull Context context, @NonNull AttributeSet attrs)
	// 
	public FlowLayout(@NonNull Context context, @NonNull AttributeSet attrs, int 		defStyleAttr) 

	@TargetApi(21)
	public FlowLayout(@NonNull Context context, @NonNull AttributeSet attrs, int 		defStyleAttr, int defStyleRes) 
}
1.定义容器组件要配置的属性

在value/attrs.xml文件中添加声明

<resources>
    <attr name="divide" format="dimension" />
    <declare-styleable name="FlowLayout">
        <attr name="horizontalDivide" format="dimension">0dp</attr>
        <attr name="verticalDivide" format="dimension">0dp</attr>
    </declare-styleable>
</resources>

解析xml配置的属性

    private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        if (attrs != null) {
            TypedArray typedArray = this.getContext().obtainStyledAttributes
                (attrs, R.styleable.FlowLayout, defStyleAttr, defStyleRes);
            int len = typedArray.getIndexCount();
            for (int i = 0; i < len; i++) {
                int attr = typedArray.getIndex(i);
                switch (attr) {
                    case R.styleable.FlowLayout_horizontalDivide:
                        this.horizontalDivide = typedArray
                            .getDimensionPixelOffset(attr, this.horizontalDivide);
                        break;
                    case R.styleable.FlowLayout_verticalDivide:
                        this.verticalDivide = typedArray
                            .getDimensionPixelOffset(attr, this.verticalDivide);
                        break;
                    default:
                        break;
                }
            }
            // 回收属性数组
            typedArray.recycle();
        }
    }
2.定义容器内子控件可以配置的属性。
    <declare-styleable name="FlowLayout_Layout">
        <attr name="is_new_line" format="boolean">false</attr>
    </declare-styleable>
3.自定义实现FlowLayoutParams,并解析子控件中的布局相关参数
    static class FlowLayoutParams extends MarginLayoutParams {
        public boolean isNewLine;
		// 
        public FlowLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray typedArray = c.obtainStyledAttributes
                (attrs,R.styleable.FlowLayout_Layout);
            int len = typedArray.getIndexCount();
            for (int i = 0; i < len; i++) {
                int attr = typedArray.getIndex(i);
                switch (attr) {
                    case R.styleable.FlowLayout_Layout_is_new_line:
                        isNewLine = typedArray.getBoolean(attr, false);
                        break;
                    default:
                        break;
                }
            }
            typedArray.recycle();
        }
        public FlowLayoutParams(int width, int height) {
            super(width, height);
        }
        public FlowLayoutParams(MarginLayoutParams source) {
            super(source);
        }
        public FlowLayoutParams(LayoutParams source) {
            super(source);
        }
    }
4.重写父容器中校验及获取子控件布局实例的方法。
    @Override
    protected boolean checkLayoutParams(LayoutParams p) {
        return p instanceof FlowLayoutParams;
    }

    @Override
    protected FlowLayoutParams generateDefaultLayoutParams() {
        return new FlowLayoutParams(
            LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected FlowLayoutParams generateLayoutParams(LayoutParams p) {
        return new FlowLayoutParams(p);
    }

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

这里顺便提一下这几个generateLayoutParams是在父容器addView的时候用的,当child自身没有LayoutParams时就创建一个默认值

    public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }
5.重写onMeasure来计算子控件宽高并计算自身宽高
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //先清除之前的设置
        allChild.clear();
        itemHeightList.clear();
        usedHeight = 0;
        usedWidth = 0;

        // 记录父容器宽高
        int groupWidth = MeasureSpec.getSize(widthMeasureSpec);
        int groupWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int groupHeight = MeasureSpec.getSize(heightMeasureSpec);
        int groupHeightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 为计算行数设置的临时变量
        List<View> tmpLine = new ArrayList<>();
        int tmpLineWidth = 0;
        int tmpLineHeight = 0;

        // 遍历子View,计算每个子view的测量尺寸
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 先判断child的可见性,不可见的不参与计算
            if (child.getVisibility() == View.VISIBLE) {
                // 先拿到子view的宽高参数,这个在addView的时候会由父容器设置好值。
                // 通过子view的onMeasure,就可以得到子view自身计算的宽高了,利用该宽高可以做一次初步布局
                ViewGroup.LayoutParams params = child.getLayoutParams();
                FlowLayoutParams flowLayoutParams = null;
                //获取view的margin设置参数
                if (params instanceof FlowLayoutParams) {
                    flowLayoutParams = (FlowLayoutParams) params;
                } else {
                    //不存在时创建一个新的参数
                    //基于View本身原有的布局参数对象
                    flowLayoutParams = new FlowLayoutParams(params);
                }

                // 根据父容器的限制来计算子View的尺寸限制
                // 父size使用前要减去两边的padding
                // 1.当父容器为EXACTLY时
                //      子size>=0 则 EXACTLY + 子size
                //      子match_parent,则EXACTLY+父size
                //      子wrap_content,则AT_MOST+父size
                // 2.当父容器为AT_MOST时,尽可能大
                //      子size>=0 则 EXACTLY + 子size
                //      子match_parent,则 AT_MOST+父size
                //      子wrap_content,则 AT_MOST+父size
                // 3.当父容器为UNSPECIFIED时,没指定大小
                //      子size>=0,则 EXACTLY + 子size
                //      子match_parent,则UNSPECIFIED+0
                //      子warp_content,则UNSPECIFIED+0
                int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, flowLayoutParams.width);
                int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, flowLayoutParams.height);
                // 将父容器为子view定制的限制传递给子view,最后会调用子view自身的onMeasure,
                child.measure(childWidthSpec, childHeightSpec);
                boolean isNewLine = flowLayoutParams.isNewLine;
                if (!isNewLine && child.getMeasuredWidth() < groupWidth - paddingLeft - paddingRight - tmpLineWidth) {
                    //当前行可以放得下
                    tmpLine.add(child);
                    tmpLineWidth = tmpLineWidth + child.getMeasuredWidth() + horizontalDivide;
                    // 行高要考虑margin,行号按该行最高的item计算
                    tmpLineHeight = Math.max(tmpLineHeight, child.getMeasuredHeight() + flowLayoutParams.topMargin + flowLayoutParams.bottomMargin);
                    // 如果时最后一个元素了,更新参数
                    if (i == childCount - 1) {
                        allChild.add(tmpLine);
                        usedWidth = Math.max(usedWidth, tmpLineWidth);
                        usedHeight = usedHeight + tmpLineHeight;//最后一行不需要纵向间距
                        itemHeightList.add(tmpLineHeight);
                    }
                } else {
                    // 当前行放不下,进行换行操作
                    allChild.add(tmpLine);
                    usedWidth = Math.max(usedWidth, tmpLineWidth);//更新最大行宽
                    usedHeight = usedHeight + tmpLineHeight + verticalDivide;//更新使用过的宽度
                    itemHeightList.add(tmpLineHeight);
                    // 还原临时变量
                    tmpLine = new ArrayList<>();
                    tmpLineHeight = 0;
                    tmpLineWidth = 0;
                }
            }
        }

        int realWidth = 0;
        int realHeight = 0;
        // 现在我们拿到了子view占用的宽高,可以给父容器设置测量宽高了
        if (groupWidthMode == MeasureSpec.EXACTLY) {
            realWidth = groupWidth;
        } else {// 不论是AT_MOST 还是 UNSPECIFIED 都用子view实际占用的宽度
            realWidth = usedWidth + paddingLeft + paddingRight;
        }
        if (groupHeightMode == MeasureSpec.EXACTLY) {
            realHeight = groupHeight;
        } else {
            realHeight = usedHeight + paddingTop + paddingBottom;
        }
        setMeasuredDimension(realWidth, realHeight);
    }

6.重写onLayout来对子控件进行布局
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 一行一行摆积木,按相对位置摆放
        int lineCount = allChild.size();
        int currentLeft = paddingLeft;
        int currentTop = paddingTop;
        for (int i = 0; i < lineCount; i++) {
            List<View> line = allChild.get(i);
            for (int j = 0; j < line.size(); j++) {
                View view = line.get(j);

                ViewGroup.LayoutParams params = view.getLayoutParams();
                MarginLayoutParams marginParams = null;
                //获取view的margin设置参数
                if (params instanceof ViewGroup.MarginLayoutParams) {
                    marginParams = (ViewGroup.MarginLayoutParams) params;
                } else {
                    //不存在时创建一个新的参数
                    //基于View本身原有的布局参数对象
                    marginParams = new ViewGroup.MarginLayoutParams(params);
                }
                int left = currentLeft + marginParams.leftMargin;
                int top = currentTop + marginParams.topMargin;
                int right = left + view.getMeasuredWidth();
                int bottom = top + view.getMeasuredHeight();
                view.layout(left, top, right, bottom);
                currentLeft = currentLeft + view.getMeasuredWidth() + marginParams.rightMargin;
                if (j != line.size() - 1) {
                    currentLeft = currentLeft + horizontalDivide;
                }
            }
            currentLeft = paddingLeft;
            currentTop = currentTop + itemHeightList.get(i) + verticalDivide;
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值