效果图

自定义属性
<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
positionX = paddingStart
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
positionY += realChildViewUsedHeight
}
}
}
}
if (measureNeedExpandView) {
expandView.layout(positionX, positionY, positionX + expandView.measuredWidth, positionY + expandView.measuredHeight)
} else {
expandView.layout(0, 0, 0, 0)
}
}
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
}
})