从零开始实现GridLayout

需求背景

最近这段时间在做阳光沙滩APP (可以给我一个小星星吗??),然后捏,遇到了个问题 —— RecylerView Item 列表中间需要显示三张文章封面图片。

实现方式多种多样,可以直接写死到 xml 布局里,也可以用 GridView ,也可以用 RecyclerView 的网格布局管理器,还可以通过自定义 ViewGroup 实现。


为什么我要使用自定义 ViewGroup 实现,而不是其它的?

  • 为什么不写死 xml ?

写死 xml 固然可以,但是如果是九宫格的图片展示(玩过QQ空间的同学都应该知道吧,什么?你不玩QQ空间,那你微信总玩过吧),那不是又得用 LinearLayout 套娃了吗?一想到要套好几层,就感觉整个人都不舒服了。

  • 为什么不用 GridView

因为我就展示几个控件而已,还得去写一个适配器,emmm… 不开心??

  • 为什么不用 RecyclerView

原因和上一点差不多,而且 RecyclerView 嵌套 RecyclerView 想想都刺激??

而且考虑到其它模块也可能用到这样的控件,比如摸鱼模块显示九宫格图片这样的需求,所以自己手撸了一个九宫格布局的 ViewGoup ,你可以通过自定义参数进行更多扩展的设置~


代码实现

代码不多,一百多行而已,同学们直接看源码吧~

attrs.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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
}
}

结束语

打完收工!咦,快要到饭点了呢,逃~

欢迎同学们点赞、评论、打赏+关注啦~


从零开始实现GridLayout
https://blog.funkt.cn/2021/07/04/1411606550820265985/
作者
anjiemo
发布于
2021年7月4日
许可协议
未经作者授权,禁止转载