二十二、自定義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的大小。

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