九宮格展現圖片是不少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,重點都是先理清其中的邏輯,再編寫代碼。