需求背景
最近这段时间在做阳光沙滩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
原创发布于
阳光沙滩
,未经作者授权,禁止转载