05_自定义ViewGroup

98 阅读4分钟

转载请标明出处:blog.csdn.net/lmj62356579… ,本文出自【张鸿洋的博客】

流式布局是一种在 ViewGroup 中将子 View 从左到右依次排列的方式。如果当前行剩余空间不足,子 View 会自动换行到下一行。这种布局常用于关键字搜索、热门标签等场景。

自定义 ViewGroup 实现流式布局的关键步骤包括:

  1. 重写 onMeasure 方法:完成子 View 的测量,并根据子 View 的测量结果确定 ViewGroup 自身的测量宽高。
  2. 重写 onLayout 方法:确定子 View 的位置,并调用 child.layout(left, top, right, bottom) 方法进行布局。

1. 实现 onMeasure 方法

首先,需要实现 ViewGroup 的 onMeasure 方法。主要处理两个问题:将测量过程传递到子 View 和确定 ViewGroup 自己的测量宽高。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    //ViewGroup可用宽度
    int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
    //ViewGroup可用高度
    int availableHeight = heightSize - getPaddingTop() - getPaddingBottom();

    //记录行宽,需要根据子View的宽度计算,但是不能超过availableWidth(ViewGroup可用宽度)
    int lineWidth = 0;
    //记录行高,需要根据子View的宽度计算,但是不能超过availableHeight(ViewGroup可用高度)
    int lineHeight = 0;

    //记录ViewGroup宽度,需要根据每行的宽度计算
    int width = 0;
    //记录ViewGroup高度,需要根据每行的高度计算
    int height = 0;


    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {

        View child = getChildAt(i);
     
        //将测量过程传递到子View
        measureChild(child, widthMeasureSpec, heightMeasureSpec);

        MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
        int childWidthWithMargin =
                child.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
        int childHeightWithMargin =
                child.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;


        //如果子View的宽度+当前行宽度,大于ViewGroup的可用宽度
        //那么就表示子View需要换行
        if (lineWidth + childWidthWithMargin > availableWidth) {
            //换行需要重新初始化行的宽高。
            //但是初始化行的宽高之前,需要比较上一行宽度和记录的ViewGroup宽度,两者取最大值,并累加上一行的高度
            width = Math.max(width, lineWidth);
            height += lineHeight;
            //
            lineWidth = 0;
            lineHeight = 0;
        }

        //累加行宽度
        lineWidth += childWidthWithMargin;
        //获取当前行最大高度
        lineHeight = Math.max(lineHeight, childHeightWithMargin);

    }

    //如果是最后一个View,需要比较最后一行的宽度和记录的ViewGroup宽度,两者取最大值,并累加最后一行的高度
    width = Math.max(width, lineWidth);
    height += lineHeight;

    height = Math.min(height, availableHeight);

    //根据模式,如果是MeasureSpec.EXACTLY则直接使用父ViewGroup传入的宽和高,否则设置为自己计算的宽和高
    int measureWidth = widthMode == MeasureSpec.EXACTLY ?
            widthSize : width + getPaddingLeft() + getPaddingRight();
    int measureHeight = heightMode == MeasureSpec.EXACTLY ?
            heightSize : height + getPaddingTop() + getPaddingBottom();

    //设置ViewGroup的测量宽高
    setMeasuredDimension(measureWidth, measureHeight);
}

上述代码通过测量子 View 的尺寸,并计算出 ViewGroup 的最终宽高。

2. 实现 onLayout 方法

接下来,实现 onLayout 方法,确定每一个子 View 的位置。

//存储所有的View
protected List<List<View>> mAllViews = new ArrayList<>();
//存储当前行所有的View
protected List<View> mLineViews = new ArrayList<>();
//存储每一行的行宽
protected List<Integer> mLineWidths = new ArrayList<>();
//存储每一行的行高
protected List<Integer> mLineHeights = new ArrayList<>();

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    mAllViews.clear();
    mLineViews.clear();
    mLineWidths.clear();
    mLineHeights.clear();

    //记录当前行宽度
    int lineWidth = 0;
    //记录当前行高度
    int lineHeight = 0;

    //ViewGroup的可用宽度
    int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();

    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);

        MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
        //子View的宽度加左右margin值
        int childWidthWithMargin = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
        //子View的高度加左右margin值
        int childHeightWithMargin = child.getMeasuredHeight() + params.topMargin + params.topMargin;

        //换行
        if (lineWidth + childWidthWithMargin > availableWidth) {
            //记录上一行的宽度、高度以及View的一个集合
            mLineHeights.add(lineHeight);
            mLineWidths.add(lineWidth);
            mAllViews.add(mLineViews);

            //换行后,重新初始化记录的行宽高,初始化当前行的View集合
            lineWidth = 0;
            lineHeight = 0;
            mLineViews = new ArrayList<>();
        }

        //累加子View的宽度
        lineWidth += childWidthWithMargin;
        //比较高度
        lineHeight = Math.max(lineHeight, childHeightWithMargin);
        //记录改行的View
        mLineViews.add(child);
    }

    //for循环中没有记录最后一行的宽高,需要单纯记录
    mLineHeights.add(lineHeight);
    mLineWidths.add(lineWidth);
    mAllViews.add(mLineViews);

    /**
     * 开始摆放View
     * */
    
    //第一个View的top位置
    int top = getPaddingTop();
    //遍历每一行
    for (int i = 0; i < mAllViews.size(); i++) {
        //获取当前行的View集合
        List<View> lineViews = mAllViews.get(i);
        lineHeight = mLineHeights.get(i);
        lineWidth = mLineWidths.get(i);

        //每行第一个View的left位置
        int left = getPaddingLeft();
        //遍历每一行中的View
        for (int j = 0; j < lineViews.size(); j++) {
            View child = lineViews.get(j);
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            //确定子View的left、top、right、bottom
            int childLeft = left + params.leftMargin;
            int childTop = top + params.topMargin;
            int childRight = childLeft + childWidth;
            int childBottom = childTop + childHeight;


            child.layout(childLeft, childTop, childRight, childBottom);
            //初始化下一个View的left位置
            left = childRight + params.rightMargin;
        }
        //换行后,top需要累加上一行的高度
        top += lineHeight;
    }
}

上述代码通过计算每一行子 View 的位置,并在换行时重新计算位置。

3. 使用 FlowLayout

最后,在布局文件中使用自定义的 FlowLayout:

<com.example.viewserise.flow.TagFlowLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    android:background="#eeeeee"
    android:padding="10dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:background="@drawable/bg_test"
        android:paddingStart="15dp"
        android:paddingTop="8dp"
        android:paddingEnd="15dp"
        android:paddingBottom="8dp"
        android:text="呵呵呵"
        android:textColor="#ffffff"
        android:textSize="14sp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:background="@drawable/bg_test"
        android:paddingStart="15dp"
        android:paddingTop="8dp"
        android:paddingEnd="15dp"
        android:paddingBottom="8dp"
        android:text="旅行箱"
        android:textColor="#ffffff"
        android:textSize="14sp" />

    <!-- 更多的 TextView 元素... -->

</com.example.viewserise.flow.TagFlowLayout>

View系列文章

01_View基础知识

02_View的滑动

03_View的事件分发机制

04_View的工作流程

05_自定义View

05_自定义ViewGroup

06_View滑动冲突处理