二十二、自定义ViewGroup(流式布局,类似flexbox效果)

一、谷歌现有FlexboxLayout的效果

二、自定义实现一个简单的版本 MyFlowView(支持margin)

  • 效果
  • 直接贴源码
package com.haiheng.myapplication

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

class MyFlowView(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {

    val TAG = "MyFlowView"

    /**
     * 存所有的子View,每一个元素 就是一行
     *
     */
    var childListView = mutableListOf<List<View>>()

    /**
     * 装一行
     */
    var childLineListView = mutableListOf<View>()

    /**
     * 每一行的高
     */
    var lineHeights = mutableListOf<Int>()

    /**
     * 父亲给我的宽高
     */
    var selWidth = 0
    var selHeight = 0


    /**
     * 测量
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        selWidth = MeasureSpec.getSize(widthMeasureSpec)
        selHeight = MeasureSpec.getSize(heightMeasureSpec)
        childListView = mutableListOf<List<View>>()
        childLineListView = mutableListOf<View>()
        lineHeights = mutableListOf<Int>()
        /**
         * 孩子希望的宽高
         */
        var childNeedWidth = 0
        var childNeedHeight = 0

        /**
         * 最终确定的宽高
         */
        var finalWidth = 0
        var finalHeight = 0

        //当前行的宽度
        var currentLineWidth = 0


        //1、获取所有的子View
        for (i in 0..(childCount - 1)) {

            val childView = getChildAt(i)
            if (childView.visibility != GONE) {
                //2、获取子view的测量规格
                val layoutParams = childView.layoutParams as MarginLayoutParams

                val childWidthMeasureSpec = getChildMeasureSpec(
                    widthMeasureSpec,
                    paddingLeft + paddingRight,
                    layoutParams.width
                )
                val childHeightMeasureSpec = getChildMeasureSpec(
                    heightMeasureSpec,
                    paddingTop + paddingBottom,
                    layoutParams.height
                )
                /**
                 * 测量子View
                 */
                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)

                /**
                 * 当前行的宽
                 */

                Log.e(TAG, "父亲给的宽度${selWidth} and 当前孩子的宽度:${childView.measuredWidth}")
                currentLineWidth =
                    currentLineWidth + childView.measuredWidth + layoutParams.leftMargin + layoutParams.rightMargin
                //未换行
                if (currentLineWidth < selWidth) {
                    Log.e(TAG, "currentLineWidth = ${currentLineWidth}")
                    //装在一行里面
                    childLineListView.add(childView)


                }
                //换行
                else {
                    //装入当前行
                    childListView.add(childLineListView)
                    childLineListView = mutableListOf<View>()
                    childLineListView.add(childView)
                    currentLineWidth = childView.measuredWidth
                }
                if (i == (childCount - 1)) {
                    //如果是最后一行
                    childListView.add(childLineListView)
                }

            }


        }
        childNeedWidth = getChildNeeddWidth()
        childNeedHeight = getChildNeeddHeight()


        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
            finalWidth = selWidth
        } else {
            finalWidth = childNeedWidth
        }
        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
            finalHeight = selHeight
        } else {
            finalHeight = childNeedHeight
        }
        setMeasuredDimension(finalWidth, finalHeight)
    }

    /**
     * 获取孩子需要的宽
     */
    private fun getChildNeeddWidth(): Int {
        val lineList = mutableListOf<Int>()
        childListView.forEach {
            // 获取当前行的和
            val lineWidth = getLineWith(it)
            lineList.add(lineWidth)
        }
        //获取所有行最长的
        var width = 0
        lineList.forEach {
            if (it > width) {
                width = it
            }
        }
        return width

    }

    /**
     * 获取当前行的和
     */
    private fun getLineWith(it: List<View>): Int {
        var width = 0
        it.forEach {
            width = width + it.measuredWidth
        }
        return width
    }

    /**
     * 获取孩子需要的高
     */
    private fun getChildNeeddHeight(): Int {
        var height = 0
        childListView.forEach {

            height = height + getMaxHeight(it)
            lineHeights.add(getMaxHeight(it))
        }
        return height
    }

    /**
     * 获取当前行最高的
     */
    private fun getMaxHeight(it: List<View>): Int {
        var maxHeight = 0
        it.forEach {
            val layoutParams = it.layoutParams as MarginLayoutParams
            val marginHeight = layoutParams.topMargin + layoutParams.bottomMargin

            if ((it.measuredHeight + marginHeight) > maxHeight) {

                maxHeight = it.measuredHeight + marginHeight
            }
        }
        return maxHeight;
    }


    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {

        //布局
        var cLeft = paddingLeft
        var cTop = paddingTop
        for (i in 0..(childListView.size - 1)) {
            /**
             * 当前行的所有view
             */
            val lineViews = childListView.get(i)

            /**
             * 当前行的高
             */
            val lineHight = lineHeights.get(i)

            lineViews.forEach { view ->
                val layoutParams = view.layoutParams as MarginLayoutParams

                val left = cLeft + layoutParams.leftMargin
                val top = cTop + layoutParams.topMargin
                val right = view.measuredWidth + left
                val bottom = view.measuredHeight + top
                view.layout(left, top, right, bottom)
                cLeft = right + layoutParams.rightMargin

            }
            cLeft = paddingLeft
            cTop = lineHight + cTop

        }


    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams? {
        return MarginLayoutParams(context, attrs)
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.haiheng.myapplication.MyFlowView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView

            android:layout_marginBottom="10dp"
            android:layout_marginTop="10dp"
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="水果味孕妇奶粉" />

        <TextView
            android:layout_marginBottom="20dp"
            android:layout_marginTop="20dp"
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="儿童洗衣机" />

        <TextView
            android:layout_marginTop="40dp"
            android:layout_marginBottom="50dp"
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="洗衣机全自动" />

        <TextView
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="小度" />

        <TextView
            android:layout_marginTop="60dp"
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="儿童汽车可坐人" />

        <TextView
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="抽真空收纳袋" />

        <TextView
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="儿童滑板车" />

        <TextView
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="稳压器 电容" />



        <TextView
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="羊奶粉" />


        <TextView
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="奶粉1段" />

        <TextView
            android:layout_marginTop="50dp"
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_button_circular"
            android:text="图书勋章日" />

    </com.haiheng.myapplication.MyFlowView>

</LinearLayout>

三、总结

  • 自定ViewGroup通常实现onLayout、onMearsue 方法即可,因为自定义的是容器,不需要绘制。
  • onMeasure可能由于父亲的调用多次,触发被多次调用,所以我们保存数据的成员变量,记得在onMeasure清空,用最后一次测量的为准。
  • 首先要确定我们自定义ViewGroup的大小,而一个ViewGroup的大小由自身的MeasureSpec和所有的子View的大小决定,而子View的大小由自身的LayoutParams(布局属性)和父亲的MeasureSpec,padding 决定
  • 所以第一步要根据父亲的MeasureSpec,padding 测量所有的子View大小,使用一个集合装起来
   //2、获取子view的测量规格
                val layoutParams = childView.layoutParams as MarginLayoutParams

                val childWidthMeasureSpec = getChildMeasureSpec(
                    widthMeasureSpec,
                    paddingLeft + paddingRight,
                    layoutParams.width
                )
                val childHeightMeasureSpec = getChildMeasureSpec(
                    heightMeasureSpec,
                    paddingTop + paddingBottom,
                    layoutParams.height
                )
                /**
                 * 测量子View
                 */
                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
  • 拿到所有子View大小之后,根据自身的MeasureSpec,计算出最终ViewGroup的大小,然后设置到自身
   int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth: parentNeededWidth;
        int realHeight = (heightMode == MeasureSpec.EXACTLY) ?selfHeight: parentNeededHeight;
        setMeasuredDimension(realWidth, realHeight);
  • 测量之后就要布局,这个时候可以根据我们自定义ViewGroup算法,把我们的子View放到合适的座标位置
 //布局
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int lineCount = allLines.size();

        int curL = getPaddingLeft();
        int curT = getPaddingTop();

        for (int i = 0; i < lineCount; i++){
            List<View> lineViews = allLines.get(i);

            int lineHeight = lineHeights.get(i);
            for (int j = 0; j < lineViews.size(); j++){
                View view = lineViews.get(j);
                int left = curL;
                int top =  curT;

//                int right = left + view.getWidth();
//                int bottom = top + view.getHeight();

                 int right = left + view.getMeasuredWidth();
                 int bottom = top + view.getMeasuredHeight();
                 view.layout(left,top,right,bottom);
                 curL = right + mHorizontalSpacing;
            }
            curT = curT + lineHeight + mVerticalSpacing;
            curL = getPaddingLeft();
        }

    }
  • 使用Margin的时候注意
    (1)在自定义View类中重写generateLayoutParams方法
    (2)使用view.layoutParams方法拿到margin
   val layoutParams = view.layoutParams as MarginLayoutParams

                val left = cLeft + layoutParams.leftMargin
                val top = cTop + layoutParams.topMargin

(3)使用margin的时候,我们子View的具体大小不变,但是在设置自定义ViewGroup的大小的时候,记得加上margin的大小,并且在布局的时候,也需要考虑margin的大小。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章