Android FlowLayout实现流式布局 kotlin实现FlowLayout 自定义ViewGroup

最近在看viwe的自定义,看到蛮多讲解FlowLayout自定义ViewGroup的例子,然后回顾了一下《Android自定义控件开发入门与实战》里面的这个例子,想找一个kotlin的顺便看看,但是都没有,所以就写了这个博客记录一下

自定义view的文章应该很多,我就简单记录一下重点

一个是用java写的 一个是用kotlin写的 最近在学习kotlin

效果图

在这里插入图片描述

坐标位置的计算关系

在这里插入图片描述

一、 View的绘制一般遵循

构造函数->onMeasure()(测量View的大小)-onSizeChanged()()->onLayout()(确定子View布局)->onDraw()(开始绘制内容)->invalidate()(重绘刷新)
 view主要实现:onMeasure() + onDraw()
 vierGroup主要实现:onMeasure()+onLayout()

在这里插入图片描述
(图是截图自:https://blog.csdn.net/heng615975867/article/details/80379393)

二、重点 是onMeasure()方法计算容器的宽高,onLayout()方法计算每个childViwe的位置(left,top,right,bottom),并且调用view.onLayout()方法绘制

步骤一

要提取margin值,就一定要先重写generateLayoutParams()方法
要提取margin值,就一定要先重写generateLayoutParams()方法
要提取margin值,就一定要先重写generateLayoutParams()方法

    /**
     * 重写generateLayoutParams方法 为了提取Margin
     *
     * @param p
     * @return
     */

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

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

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

步骤二 onMeasure() 方法 计算容器宽高设置到setMeasuredDimension()

计算容器的宽高,需要根据遍历所有的childView来确定最大的宽高,同时需要根据measureWidthMode来判断
思路就是记录当前行的宽高,然后for循环计算每个childView的情况,取出最后的最大width和height,然后重点调用 setMeasuredDimension()方法

        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

        int lineWidth = 0;//记录每一行的宽度
        int linHeight = 0;//记录每一行的高度
        int totalWidth = 0;//记录整体的宽度
        int totalHeight = 0;//记录整体的高度
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            //一定要先调用measureChild(),调用getMeasuredWidth() 才生效
            measureChild(view, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
            int viewWidth = view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int viewHeight = view.getHeight() + lp.topMargin + lp.bottomMargin;
            if (lineWidth + viewWidth > measureWidth) { //当前的行宽+child的宽大于最大的测量宽度
                //换行的情况
                totalWidth = Math.max(lineWidth, viewWidth);
                totalHeight += linHeight;
                lineWidth = viewWidth;
                linHeight = viewHeight;
            } else {
                //不换行的情况
                linHeight = Math.max(linHeight, viewHeight);
                lineWidth += viewWidth;
            }
            if (i == count - 1) {
                totalHeight += linHeight;
                totalWidth = Math.max(totalWidth, lineWidth);
            }
        }
        //所以的工作都是为了确定容器的宽高
        setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : totalWidth, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : totalHeight);

步骤三 onLayout() 方法中记录每一个childView的位置保存,然后根据坐标调用view.layout(l,t,r,b)绘制出view

        int count = getChildCount();
        int lineWidth = 0;//累加当前行的行宽
        int linwHeight = 0;//累加当前的行高
        int top = 0, left = 0;//当前空间的top坐标和left坐标
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
            int viewWidth = view.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
            int viewHeight = view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (viewWidth + lineWidth > getMeasuredWidth()) {
                //如果换行
                top += linwHeight;
                left = 0;
                linwHeight = viewHeight;
                lineWidth = viewWidth;
            } else {
                linwHeight = Math.max(linwHeight, viewHeight);
                lineWidth += viewWidth;
            }
            //计算view的left top right bottom
            int lc = left + lp.leftMargin;
            int tc = top + lp.topMargin;
            int rc = lc + view.getMeasuredWidth();
            int bc = tc + view.getMeasuredHeight();
            view.layout(lc, tc, rc, bc);
            //将left置为下一个子控件的起点
            left += viewWidth;

使用

kotlin版本类FlowLayoutKotlin

package com.example.flowlayoutdemo

import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup

class FlowLayoutKotlin : ViewGroup {

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def)

    override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
        return MarginLayoutParams(p)
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }

    override fun generateDefaultLayoutParams(): LayoutParams {
        return MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT)
    }


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        val measureWidth = MeasureSpec.getSize(widthMeasureSpec)
        val measureHeight = MeasureSpec.getSize(heightMeasureSpec)
        val measureWidthMode = MeasureSpec.getMode(widthMeasureSpec)
        val measureHeightMode = MeasureSpec.getMode(heightMeasureSpec)

        var lineWidth = 0 //记录每一行的宽度
        var linHeight = 0 //记录每一行的高度
        var totalWidth = 0 //记录整体的宽度
        var totalHeight = 0 //记录整体的高度
        val count = childCount
        for (i in 0 until count) {
            val view = getChildAt(i)
            //一定要先调用measureChild(),调用getMeasuredWidth() 才生效
            measureChild(view, widthMeasureSpec, heightMeasureSpec)
            val lp = view.layoutParams as MarginLayoutParams
            val viewWidth = view.measuredWidth + lp.leftMargin + lp.rightMargin
            val viewHeight = view.height + lp.topMargin + lp.bottomMargin
            if (lineWidth + viewWidth > measureWidth) { //当前的行宽+child的宽大于最大的测量宽度
                //换行的情况
                totalWidth = Math.max(lineWidth, viewWidth)
                totalHeight += linHeight
                lineWidth = viewWidth
                linHeight = viewHeight
            } else {
                //不换行的情况
                linHeight = Math.max(linHeight, viewHeight)
                lineWidth += viewWidth
            }
            if (i == count - 1) {
                totalHeight += linHeight
                totalWidth = Math.max(totalWidth, lineWidth)
            }
        }
        //所以的工作都是为了确定容器的宽高
        //所以的工作都是为了确定容器的宽高
        setMeasuredDimension(
            if (measureWidthMode == MeasureSpec.EXACTLY) measureWidth else totalWidth,
            if (measureHeightMode == MeasureSpec.EXACTLY) measureHeight else totalHeight
        )


    }
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //四个参数 当前行的宽高  容器的累计宽高 即宽度是取能获取的最大值,高度方向是累加的值

        val count = childCount
        var lineWidth = 0 //累加当前行的行宽

        var linwHeight = 0 //累加当前的行高

        var top = 0
        var left = 0 //当前空间的top坐标和left坐标

        for (i in 0 until count) {
            val view = getChildAt(i)
            val lp = view.layoutParams as MarginLayoutParams
            val viewWidth = view.measuredWidth + lp.rightMargin + lp.leftMargin
            val viewHeight = view.measuredHeight + lp.topMargin + lp.bottomMargin
            if (viewWidth + lineWidth > measuredWidth) {
                //如果换行
                top += linwHeight
                left = 0
                linwHeight = viewHeight
                lineWidth = viewWidth
            } else {
                linwHeight = Math.max(linwHeight, viewHeight)
                lineWidth += viewWidth
            }
            //计算view的left top right bottom
            val lc = left + lp.leftMargin
            val tc = top + lp.topMargin
            val rc = lc + view.measuredWidth
            val bc = tc + view.measuredHeight
            view.layout(lc, tc, rc, bc)
            //将left置为下一个子控件的起点
            left += viewWidth
        }


    }


}

java版本的类FlowLayoutJava

package com.example.flowlayoutdemo;

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

public class FlowLayoutJava extends ViewGroup {

    public FlowLayoutJava(Context context) {
        super(context);
    }

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

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


    /**
     * 重写generateLayoutParams方法 为了提取Margin
     *
     * @param p
     * @return
     */

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

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

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

        int lineWidth = 0;//记录每一行的宽度
        int linHeight = 0;//记录每一行的高度
        int totalWidth = 0;//记录整体的宽度
        int totalHeight = 0;//记录整体的高度
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            //一定要先调用measureChild(),调用getMeasuredWidth() 才生效
            measureChild(view, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
            int viewWidth = view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int viewHeight = view.getHeight() + lp.topMargin + lp.bottomMargin;
            if (lineWidth + viewWidth > measureWidth) { //当前的行宽+child的宽大于最大的测量宽度
                //换行的情况
                totalWidth = Math.max(lineWidth, viewWidth);
                totalHeight += linHeight;
                lineWidth = viewWidth;
                linHeight = viewHeight;
            } else {
                //不换行的情况
                linHeight = Math.max(linHeight, viewHeight);
                lineWidth += viewWidth;
            }
            if (i == count - 1) {
                totalHeight += linHeight;
                totalWidth = Math.max(totalWidth, lineWidth);
            }
        }
        //所以的工作都是为了确定容器的宽高
        setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : totalWidth, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : totalHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int lineWidth = 0;//累加当前行的行宽
        int linwHeight = 0;//累加当前的行高
        int top = 0, left = 0;//当前空间的top坐标和left坐标
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
            int viewWidth = view.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
            int viewHeight = view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (viewWidth + lineWidth > getMeasuredWidth()) {
                //如果换行
                top += linwHeight;
                left = 0;
                linwHeight = viewHeight;
                lineWidth = viewWidth;
            } else {
                linwHeight = Math.max(linwHeight, viewHeight);
                lineWidth += viewWidth;
            }
            //计算view的left top right bottom
            int lc = left + lp.leftMargin;
            int tc = top + lp.topMargin;
            int rc = lc + view.getMeasuredWidth();
            int bc = tc + view.getMeasuredHeight();
            view.layout(lc, tc, rc, bc);
            //将left置为下一个子控件的起点
            left += viewWidth;
        }
    }
}

1.创建textview_shape

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    android:shape="rectangle">
    <corners android:radius="3dp" />
    <stroke android:color="#cc0033" android:width="1dp"/>
    <padding android:top="2dp" android:bottom="2dp" android:left="2dp" android:right="2dp" />
</shape>

2.布局中引用

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.flowlayoutdemo.FlowLayoutJava
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"
        android:background="@drawable/textview_shape">

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="白茶清欢无别事"
            android:textSize="20sp" />

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="我在等风也等你"
            android:textSize="20sp" />

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="山高决定人为峰"
            android:textSize="20sp" />

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="海阔无涯天作岸"
            android:textSize="20sp" />

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="不应该"
            android:textSize="20sp" />

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="在"
            android:textSize="20sp" />

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="自怜中沉沦"
            android:textColor="@android:color/black"
            android:textSize="20sp" />

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="每一次困难"
            android:textSize="20sp" />

        <TextView
            style="@style/text_flow_style"
            android:background="@drawable/textview_shape"
            android:text="都看成是可以改变的机会"
            android:textSize="20sp" />
    </com.example.flowlayoutdemo.FlowLayoutJava>

</androidx.constraintlayout.widget.ConstraintLayout>

猜你喜欢

转载自blog.csdn.net/qq_38355313/article/details/115484709