需求背景
最近这段时间在做阳光沙滩APP (可以给我一个小星星吗??),然后捏,遇到了个问题 —— RecylerView Item 列表中间需要显示三张文章封面图片。
实现方式多种多样,可以直接写死到 xml 布局里,也可以用 GridView ,也可以用 RecyclerView 的网格布局管理器,还可以通过自定义 ViewGroup 实现。
为什么我要使用自定义 ViewGroup 实现,而不是其它的?
- 为什么不写死 xml ?
 
写死 xml 固然可以,但是如果是九宫格的图片展示(玩过QQ空间的同学都应该知道吧,什么?你不玩QQ空间,那你微信总玩过吧),那不是又得用 LinearLayout 套娃了吗?一想到要套好几层,就感觉整个人都不舒服了。
- 为什么不用 
GridView? 
因为我就展示几个控件而已,还得去写一个适配器,emmm... 不开心??
- 为什么不用 
RecyclerView? 
原因和上一点差不多,而且 RecyclerView 嵌套 RecyclerView 想想都刺激??
而且考虑到其它模块也可能用到这样的控件,比如摸鱼模块显示九宫格图片这样的需求,所以自己手撸了一个九宫格布局的 ViewGoup ,你可以通过自定义参数进行更多扩展的设置~

代码实现
代码不多,一百多行而已,同学们直接看源码吧~
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="GridLayout">
        <!--最大 item 个数-->
        <attr name="maxItemCount" format="integer" />
        <!--一行显示的最大个数-->
        <attr name="spanCount" format="integer" />
        <!--每一列之间的横向间隔-->
        <attr name="horizontalSpace" format="dimension" />
        <!--每一列之间的纵向间隔-->
        <attr name="verticalSpace" format="dimension" />
        <!--最大 item 行数-->
        <attr name="maxItemLines" format="integer" />
    </declare-styleable>
</resources>
GridLayout.kt
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import cn.cqautotest.sunnybeach.R
/**
 *    author : A Lonely Cat
 *    github : https://github.com/anjiemo/SunnyBeach
 *    time   : 2021/7/4
 *    desc   : 默认按照九宫格摆放的自定义控件(可以自行设置相关参数)
 */
class GridLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
    // 默认的最大显示数目
    var maxItemCount = DEFAULT_MAX_ITEM_COUNT
        set(value) {
            if (value < 0) {
                throw IllegalArgumentException("Maximum display number cannot be less than 0!")
            }
            field = value
        }
    // 默认的最大行数
    var maxItemLines = DEFAULT_MAX_ITEM_LINES
    // 一行摆放的个数,默认为 3
    var spanCount = DEFAULT_SPAN_COUNT
    // 横向的间距
    var horizontalSpace = DEFAULT_HORIZONTAL_SPACE
    // 纵向的间距
    var verticalSpace = DEFAULT_VERTICAL_SPACE
    // 最后一个 childView
    private lateinit var _lastChildView: View
    val lastChildView: View get() = _lastChildView
    init {
        val ta = context.obtainStyledAttributes(attrs, R.styleable.GridLayout)
        maxItemCount =
            ta.getInteger(R.styleable.GridLayout_maxItemCount, DEFAULT_MAX_ITEM_COUNT)
        maxItemLines =
            ta.getInteger(R.styleable.GridLayout_maxItemLines, DEFAULT_MAX_ITEM_LINES)
        spanCount = ta.getInteger(R.styleable.GridLayout_spanCount, DEFAULT_SPAN_COUNT)
        verticalSpace =
            ta.getDimensionPixelSize(R.styleable.GridLayout_verticalSpace, DEFAULT_VERTICAL_SPACE)
        horizontalSpace =
            ta.getDimensionPixelSize(R.styleable.GridLayout_horizontalSpace, DEFAULT_HORIZONTAL_SPACE)
        ta.recycle()
    }
    /**
     * 设置网格视图的 item 列表
     */
    fun setGridViews(vararg views: View) {
        setGridViews(views.toList())
    }
    /**
     * 设置网格视图的 item 列表
     */
    fun setGridViews(views: List<View>) {
        removeAllViews()
        for (view in views) {
            addView(view)
        }
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 获取自己的宽度
        val width = MeasureSpec.getSize(widthMeasureSpec)
        // 计算 childView 的宽度
        val itemWidth: Int =
            (width - paddingLeft - paddingRight - horizontalSpace * (spanCount - 1)) / spanCount
        var childCount = childCount
        // 计算最大显示的item个数
        childCount = childCount.coerceAtMost(maxItemCount)
        if (childCount <= 0) {
            setMeasuredDimension(0, 0)
            return
        }
        for (i in 0 until childCount) {
            val childView: View = getChildAt(i)
            // 测量 childView
            val itemSpec = MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY)
            // 摆放 childView
            measureChild(childView, itemSpec, itemSpec)
        }
        val height: Int =
            (itemWidth * (if (childCount % spanCount == 0) childCount / spanCount else childCount / spanCount + 1) + verticalSpace * ((childCount - 1) / spanCount))
        // 指定自己的宽高
        setMeasuredDimension(width, height)
    }
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var count = childCount
        // 计算需要摆放的个数,个数最小原则
        count = count.coerceAtMost(maxItemCount)
        // 如果没有 childView ,则无需摆放
        if (count < 1) return
        // 设置起始位置
        var cl = paddingLeft
        var ct = paddingTop
        // 起始行数
        var lines = 1
        for (index in 0 until count) {
            if (lines == NOT_LIMITED_LINES || lines > maxItemLines) continue
            val targetView = getChildAt(index)
            // 判断是否需要跳过该 View
            if (needSkipView(targetView)) continue
            // 测量 childView
            val width = targetView.measuredWidth
            val height = targetView.measuredHeight
            // 摆放 childView
            targetView.layout(cl, ct, cl + width, ct + height)
            // 累加 childView 的宽度
            cl += width + horizontalSpace
            // 判断是否需要换行(是否已经到了最右边)
            if ((index + 1) % spanCount == 0) {
                // 重置 childView 左边的起始位置
                cl = paddingLeft
                // 叠加 childView 的高度
                ct += height + verticalSpace
                lines++
            }
        }
        // 获取最后一个 childView
        _lastChildView = getChildAt(count - 1)
    }
    /**
     * 判断是否需要跳过该 View
     */
    private fun needSkipView(targetView: View) = targetView.visibility == View.GONE
    companion object {
        // 默认的最大显示的 item 个数
        const val DEFAULT_MAX_ITEM_COUNT = 9
        // 默认一行显示的 item 个数
        const val DEFAULT_SPAN_COUNT = 3
        // 默认显示的最大 item 行数
        const val DEFAULT_MAX_ITEM_LINES = 3
        // 不限制行数
        const val NOT_LIMITED_LINES = -1
        // item 之间的横向间距
        const val DEFAULT_HORIZONTAL_SPACE = 0
        // item 之间的纵向间距
        const val DEFAULT_VERTICAL_SPACE = 0
    }
}
结束语
打完收工!咦,快要到饭点了呢,逃~
欢迎同学们点赞、评论、打赏+关注啦~
          本文由
          A lonely cat
          原创发布于
          阳光沙滩
          ,未经作者授权,禁止转载
        
 


 A lonely cat  回复 @拉大锯