九宫格展现图片是不少APP的经常使用功能,固然实现方式有不少种。这里我们选择自定义ViewGroup来实现。作一个抛砖引玉的效果,理解自定义ViewGroup的经常使用流程。markdown
首先分析九宫格的基本布局逻辑:app
1
张图片的时候,布局中的ImageView
会根据图片自己的宽高比呈现为横图或竖图1
张的时候,布局中的ImageView
的宽高会固定为布局宽度的(减去了图片之间的间距)1/3
大小,并呈3*3
现网格布局。4
张图片的时候,图片View的宽高会固定为布局宽度的(减去了图片之间的间距)1/3
大小,但网格只有两列。从上面的分析,咱们能够发现当只有一张图片的时候,为了肯定ImageView
的大小,须要知道图片的宽高,那么首先定义一个图片接口:ide
interface GridImage {
//图片的宽
fun getWidth(): Int
//图片的高
fun getHeight(): Int
//图片地址
fun getUri(): Uri?
}
复制代码
新建一个GridImageLayout
类继承自ViewGroup
,重写onMeasure
方法和onLayout
方法。布局
首先定义几个变量方便后续工做:this
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需求本身定义。spa
而当有多张图片的时候,宽度有两种状况须要考虑:code
Wrap_Content
模式,宽度根据实际展现的列数乘以每列的宽度Match_Parent
模式,宽度直接设定为系统测量到的值但其实不多有使用Wrap_Content
模式的场景,因此这里不考虑。orm
除了须要肯定自身的大小觉得,还须要肯定每一个子View的大小。子View大小逻辑在分析中已经能够得出。xml
理清逻辑后,则编码的工做就简单了。代码以下:
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)
}
}
复制代码
上面两个方法写完后,就已经完成了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,重点都是先理清其中的逻辑,再编写代码。