引言
九宫格展示图片是很多APP的常用功能,当然实现方式有很多种。这里咱们选择自定义ViewGroup来实现。做一个抛砖引玉的效果,理解自定义ViewGroup的常用流程。
分析
首先分析九宫格的基本布局逻辑:
- 当只有
1
张图片的时候,布局中的ImageView
会根据图片本身的宽高比呈现为横图或竖图 - 当大于
1
张的时候,布局中的ImageView
的宽高会固定为布局宽度的(减去了图片之间的间距)1/3
大小,并呈3*3
现网格布局。 - 特殊情况当有
4
张图片的时候,图片View的宽高会固定为布局宽度的(减去了图片之间的间距)1/3
大小,但网格只有两列。
代码实现
图片实体
从上面的分析,我们可以发现当只有一张图片的时候,为了确定ImageView
的大小,需要知道图片的宽高,那么首先定义一个图片接口:
interface GridImage {
//图片的宽
fun getWidth(): Int
//图片的高
fun getHeight(): Int
//图片地址
fun getUri(): Uri?
}
复制代码
ViewGroup实现
新建一个GridImageLayout
类继承自ViewGroup
,重写onMeasure
方法和onLayout
方法。
首先定义几个变量方便后续工作:
private val data = mutableListOf<GridImage>()//数据
private var lineCount = 0 //展示全部数据需要的行数
private val maxCount = 9 //最大支持图片数
private val maxRowCount = 3 //最多列数
private var space = 0 //图片之间的间距
复制代码
测量大小
自定义一个ViewGroup的首要任务就是要定义测量逻辑,让ViewGroup知道自己的大小,才能在屏幕上展示出来。
根据上面的分析得出:
当图片只有一张的时候,整个ViewGroup的大小和负责显示图片的ImageView是一样大的。这个大小可以根据图片的宽高比乘以一个预设的宽度或高度得到。这个预设的宽度取决于xml文件里设定或根据UI需求自己定义。
而当有多张图片的时候,宽度有两种情况需要考虑:
- 在xml文件定义为
Wrap_Content
模式,宽度根据实际展示的列数乘以每列的宽度 - 在xml文件中固定数值或
Match_Parent
模式,宽度直接设定为系统测量到的值
但其实很少有使用Wrap_Content
模式的场景,所以这里不考虑。
除了需要确定自身的大小以为,还需要确定每个子View的大小。子View大小逻辑在分析中已经可以得出。
理清逻辑后,则编码的工作就简单了。代码如下:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (data.isEmpty())
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY))
else {
val groupWidth: Float
val groupHeight: Float
val size = data.size
if (size == 1) {
//当图片只有1张的时候,最大宽为当前ViewGroup的宽80%,最大高定义为200dp
val maxWidth = MeasureSpec.getSize(widthMeasureSpec) * 0.8f
val maxHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
200f,
resources.displayMetrics
)
//可自由定制
val minWidth = maxWidth * 0.8f
val minHeight = maxHeight * 0.8f
val image = data.first()
val ratio = image.getWidth() / image.getHeight().toFloat()
val childWidth: Float
val childHeight: Float
if (ratio > 1) {
childWidth = min(maxWidth, max(minWidth, image.getWidth().toFloat()))
childHeight = childWidth / ratio
} else {
childHeight = min(maxHeight, max(minHeight, image.getHeight().toFloat()))
childWidth = childHeight * ratio
}
measureChild(childWidth.toInt(), childHeight.toInt())
groupWidth = childWidth
groupHeight = childHeight
} else {
//如果是大于两个,则child宽高为当前ViewGroup宽度的1/3
val childWidth =
(MeasureSpec.getSize(widthMeasureSpec) -
(space * (maxRowCount - 1))) / maxRowCount.toFloat()
measureChild(childWidth.toInt(), childWidth.toInt())
groupWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat()
groupHeight = (childWidth * this.lineCount) + (space * (this.lineCount - 1))
}
setMeasuredDimension(
MeasureSpec.makeMeasureSpec(groupWidth.toInt(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(groupHeight.toInt(), MeasureSpec.EXACTLY)
)
}
}
private fun measureChild(childWidth: Int, childHeight: Int) {
for (i in 0 until data.size) {
val child = getChildAt(i) ?: continue
child.measure(
MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
)
}
}
复制代码
布局
测量完成后,知道了自身和子View的大小,那么就需要确定子View该怎么排列的问题。九宫格的布局比较规律,是比较好实现的,每列最多3个view,最多3排,咱们使用一个for循环就搞定了。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (data.isEmpty())
return
for (i in 0 until data.size) {
val child = getChildAt(i)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
val currentRowIndex = i % maxRowCount
val currentLineIndex = i / maxRowCount
val marginLeft = if (currentRowIndex == 0) 0 else this.space
val marginTop = if (currentLineIndex == 0) 0 else this.space
val left = currentRowIndex * childWidth + marginLeft * currentRowIndex
val top = currentLineIndex * childHeight + marginTop * currentLineIndex
child.layout(left, top, left + childWidth, top + childHeight)
}
}
复制代码
设置数据并添加子View
上面两个方法写完后,就已经完成了90%了。但是咱们现在还没有真正往里添加ImageView
,现在暴露一个方法,设置数据并添加ImageView
//loadCallback 是加载图片的回调,由调用者实现加载图片的功能。
fun setData(
data: List<GridImage>,
loadCallback: (index: Int, view: ImageView, image: GridImage) -> Unit
) {
removeAllViewsInLayout()
this.data.clear()
if (data.size > maxCount) {
this.data.addAll(data.subList(0, maxCount))
} else {
this.data.addAll(data)
}
this.lineCount = ceil(data.size / maxRowCount.toFloat()).toInt()
for (i in data.indices) {
val imgView = ImageView(context)
addViewInLayout(
imgView, i, LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
)
)
loadCallback(i, imgView, data[i])
}
requestLayout()
}
复制代码
最后开放自定义xml属性,定义间距之类的,达到可在xml文件中自定义。
效果如下
至此,一个九宫格布局就已经实现了,是不是很简单呢。 其实无论是自定义ViewGroup还是自定义View,重点都是先理清其中的逻辑,再编写代码。