150行代码实现自定义九宫格ViewGroup

150行代码实现自定义九宫格ViewGroup

Android小彩虹2021-08-16 20:27:16180A+A-

引言

九宫格展示图片是很多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文件中自定义。

效果如下

image.png

至此,一个九宫格布局就已经实现了,是不是很简单呢。 其实无论是自定义ViewGroup还是自定义View,重点都是先理清其中的逻辑,再编写代码。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们