展开收起 FlowLayout

339 阅读3分钟
效果图

2.gif

自定义属性
<declare-styleable name="ExpandableFlowLayout">
    <attr name="default_show_row" format="integer" />
    <attr name="horizontal_margin" format="dimension" />
    <attr name="vertical_margin" format="dimension" />
</declare-styleable>
展开收起按钮布局
<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
android:layout_width="wrap_content"  
android:layout_height="wrap_content"  
android:background="@drawable/bg_expand_collapse"  
android:gravity="center_vertical"  
android:orientation="horizontal"  
android:paddingHorizontal="6dp"  
android:paddingVertical="2dp">  
  
<TextView  
android:id="@+id/tv_more_text"  
android:layout_width="wrap_content"  
android:layout_height="wrap_content"  
android:maxLines="1"  
android:text="更多"  
android:textColor="#2E363D"  
android:textSize="13sp" />  
  
<ImageView  
android:id="@+id/iv_more_arrow"  
android:layout_width="13dp"  
android:layout_height="13dp"  
android:layout_marginStart="2dp"  
android:src="@drawable/ic_expand" />  
  
</LinearLayout>


自定义ViewGroup

class ExpandableFlowLayout<T> : ViewGroup {
    private val defaultVerticalSpace = paddingTop + paddingBottom
    private val defaultHorizontalSpace = paddingStart + paddingEnd
    private var defaultShowRow = 2
    private var measureNeedExpandView = false
    private var expand = false
    private var expandView: View
    private var verticalMargin: Int
    private var horizontalMargin: Int
    private var onFlowItemClickListener: OnFlowItemClickListener<T>? = null

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandableFlowLayout)
        defaultShowRow = typedArray.getInt(R.styleable.ExpandableFlowLayout_default_show_row, 2)
        horizontalMargin = typedArray.getDimension(R.styleable.ExpandableFlowLayout_horizontal_margin, dp2px(6f)).toInt()
        verticalMargin = typedArray.getDimension(R.styleable.ExpandableFlowLayout_vertical_margin, dp2px(6f)).toInt()
        typedArray.recycle()
        expandView = LayoutInflater.from(context).inflate(R.layout.layout_label_more, null, false).apply {
            layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
            val moreText: TextView = findViewById(R.id.tv_more_text)
            val moreArrow: ImageView = findViewById(R.id.iv_more_arrow)
            moreArrow.setImageResource(R.drawable.ic_arrow_down_dark)
            moreText.text = if (!expand) "更多" else "收起"
            moreArrow.rotation = if (!expand) 0f else 180f
            setOnClickListener {
                expand = !expand
                moreText.text = if (!expand) "更多" else "收起"
                moreArrow.rotation = if (!expand) 0f else 180f
                requestLayout()
            }
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val rootWidth = MeasureSpec.getSize(widthMeasureSpec)
        var usedWidth = defaultHorizontalSpace
        var usedHeight = defaultVerticalSpace

        measureChild(expandView, widthMeasureSpec, heightMeasureSpec)
        var rowCount = 1
        for (index in 0 until childCount - 1) {
            val childView = getChildAt(index)
            if (childView != null) {
                // 测量当前子控件的宽高。
                measureChild(childView, widthMeasureSpec, heightMeasureSpec)
                val realChildViewUsedWidth = childView.measuredWidth + horizontalMargin
                val realChildViewUsedHeight = childView.measuredHeight + verticalMargin

                if (usedHeight == defaultVerticalSpace) {
                    usedHeight += realChildViewUsedHeight
                }
                // 当前子控件宽度加上之前已用宽度大于根布局宽度,需要换行。
                if (usedWidth + realChildViewUsedWidth > rootWidth) {
                    // 换行
                    rowCount++
                    // 当前为未展开状态,并且此时行数已经超过了默认显示行数,跳过后续的测量。
                    if (!expand && rowCount > defaultShowRow) {
                        break
                    }
                    // 重置已用宽度
                    usedWidth = defaultHorizontalSpace
                    // 增加已用高度
                    usedHeight += realChildViewUsedHeight
                }

                usedWidth += realChildViewUsedWidth

                if (index == childCount - 2 && expand && rowCount > defaultShowRow) {
                    // 展开状态下的最后一个元素,
                    // 此时判断能否再放下展开控件,不能则需要增加一行用于显示展开控件。
                    if (usedWidth + expandView.measuredWidth > rootWidth) {
                        usedHeight += expandView.measuredHeight + verticalMargin
                    }
                }
            }
        }
        measureNeedExpandView = rowCount > defaultShowRow
        setMeasuredDimension(rootWidth, usedHeight)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        val availableWidth = right - left
        var usedWidth = defaultHorizontalSpace
        var positionX = paddingStart
        var positionY = paddingTop
        var rowCount = 1
        for (index in 0 until childCount - 1) {
            val childView = getChildAt(index)
            if (childView != null) {
                val realChildViewUsedWidth = childView.measuredWidth + horizontalMargin
                val realChildViewUsedHeight = childView.measuredHeight + verticalMargin
                val changeRowCondition = if ((!expand && rowCount == defaultShowRow)) {
                    // 未展开状态,并且当前行已经是默认显示行,已用空间需要加上展开控件的空间
                    usedWidth + realChildViewUsedWidth + (if (measureNeedExpandView) expandView.measuredWidth else 0) > availableWidth
                } else {
                    usedWidth + realChildViewUsedWidth > availableWidth
                }
                if (changeRowCondition) {
                    // 换行
                    rowCount++
                    // 当前为未展开状态,并且此时行数已经超过了默认显示行数,跳过后续处理
                    if (!expand && rowCount > defaultShowRow) {
                        childView.layout(0, 0, 0, 0)
                        break
                    }
                    // 重置已用宽度
                    usedWidth = defaultHorizontalSpace
                    // 新行开始的x轴坐标重置
                    positionX = paddingStart
                    // 新行开始的y轴坐标增加
                    positionY += realChildViewUsedHeight
                }

                childView.layout(positionX, positionY, positionX + childView.measuredWidth, positionY + childView.measuredHeight)
                positionX += realChildViewUsedWidth
                usedWidth += realChildViewUsedWidth

                if (index == childCount - 2 && expand && rowCount > defaultShowRow) {
                    // 展开状态下的最后一个元素,
                    // 此时判断能否再放下展开控件,不能则需要增加一行用于显示展开控件。
                    if (usedWidth + expandView.measuredWidth > availableWidth) {
                        positionX = paddingStart
                        // 新行开始的y轴坐标增加
                        positionY += realChildViewUsedHeight
                    }
                }
            }
        }
        if (measureNeedExpandView) {
            expandView.layout(positionX, positionY, positionX + expandView.measuredWidth, positionY + expandView.measuredHeight)
        } else {
            expandView.layout(0, 0, 0, 0)
        }
    }

    /*   @SuppressLint("InflateParams")
       fun setData(data: List<IFlowEntity>) {
           removeAllViews()
           for (item in data) {
               LayoutInflater.from(context).inflate(R.layout.layout_flow_item, null, false).apply {
                   layoutParams = MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT)
                   setOnClickListener {
                       onItemClick?.invoke(item)
                   }
                   findViewById<AppCompatTextView>(R.id.tv_text).run {
                       text = item.getTitle()
                       gravity = Gravity.CENTER_VERTICAL
                   }
                   addView(this)
               }
           }
           addView(expandView)
       }*/
    fun setOnItemClickListener(listener: OnFlowItemClickListener<T>) {
        this.onFlowItemClickListener = listener
    }

    fun setAdapter(adapter: FlowAdapter<T>) {
        removeAllViews()
        val data = adapter.data
        for (i in data.indices) {
            val view = adapter.getView(this, data[i], i)
            if (view != null) {
                view.setOnClickListener {
                    onFlowItemClickListener?.onItemClick(data[i])
                }
                addView(view)
            }
        }
        addView(expandView)
    }

    private fun dp2px(dp: Float): Float {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
    }
}

数据适配器Adapter
abstract class FlowAdapter<T>(val data: List<T>) {
    abstract fun getView(parent: ExpandableFlowLayout<T>?, data: T, position: Int): View?
}
点击监听器
interface OnFlowItemClickListener<T> {  
    fun onItemClick(parent: ExpandableFlowLayout<T>, view: View, position: Int, item: T)  
}
使用方式
val flowLayout: ExpandableFlowLayout<String> = findViewById(R.id.flow_layout)  
  
val list = arrayListOf(  
            "Android",  
            "apple",  
            "HarmonyOS",  
            "iOS",  
            "Google",  
            "MacBook Pro",  
            "iPhone 15 Pro Max",  
            "Pixel 6L",  
            "windows",  
            "Huawei Mate60p",  
            "MacOS mojave",  
            "MacOS mojave"  
            )  
  
  
flowLayout.setAdapter(object : FlowAdapter<String>(list) {  
    override fun getView(  
            parent: ExpandableFlowLayout<String>?,  
            data: String,  
            position: Int  
        ): View? {  
        val view = LayoutInflater.from(parent?.context).inflate(R.layout.layout_flow_item, null, false)  
        val text: TextView = view.findViewById(R.id.tv_text)  
        text.text = data  
        return view  
    }  
})