The introduction

9 grid display picture is a common function of many apps, of course, there are many ways to achieve. Here we choose to customize the ViewGroup. As an example, understand the common process of customizing viewgroups.

Analysis of the

First analyze the basic layout logic of the nine grid:

  • When only1A picture of the time in the layoutImageViewDepending on the aspect ratio of the image itself, it will be rendered as a horizontal or vertical image
  • When more than1Zhang when the layoutImageViewThe width and height of the image will be fixed to the layout width (minus the spacing between images)A thirdSize, and presented3 * 3Grid layout now.
  • Special circumstances should have4– The width and height of the image View will be fixed to the layout width (minus the spacing between images)A thirdSize, but the grid has only two columns.

Code implementation

Pictures of the entity

From the above analysis, we can find that when there is only one image, in order to determine the size of the ImageView, we need to know the width and height of the image, so first define an image interface:

Interface GridImage {// picture width fun getWidth(): Int // picture height fun getHeight(): Int // picture address fun getUri(): Uri? }Copy the code
ViewGroup implementation

Create a new GridImageLayout class that inherits from ViewGroup and overrides the onMeasure and onLayout methods.

First, define several variables to facilitate follow-up work:

Private val data = mutableListOf<GridImage>() private var lineCount = 0 private val maxCount = 9 Private val maxRowCount = 3 // Maximum number of columns private var space = 0 // Space between imagesCopy the code
Measuring the size

The first task of customizing a ViewGroup is to define the measurement logic so that the ViewGroup knows its size so that it can be displayed on the screen. According to the above analysis:

When there is only one image, the entire ViewGroup is the same size as the ImageView that displays the image. This size can be obtained by multiplying the image’s aspect ratio by a preset width or height. The default width is set in the XML file or customized according to UI requirements.

When there are multiple images, the width needs to be considered in two ways:

  • The XML file is defined asWrap_ContentMode, the width is multiplied by the width of each column based on the actual number of columns displayed
  • In an XML fileFixed numericalorMatch_ParentMode, the width is set directly to the value measured by the system

But there are very few scenarios that use the Wrap_Content mode, so it’s not considered here.

In addition to determining its own size, you also need to determine the size of each child View. The subview size logic is already available in the analysis.

Once the logic is sorted out, coding is easy. The code is as follows:

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) {Float val size = data.size if (size == 1) {Float val size = data.size if (size == 1) { Widthmeasure = MeasureSpec. GetSize (widthMeasureSpec) * 0.8f val maxHeight = MeasureSpec TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 200f, Resources.displaymetrics) // Customize 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 {// 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) ) } }Copy the code
layout

After the measurement is completed, we know the size of itself and the sub-view, so we need to determine how to arrange the sub-view. 9 grid layout is more regular, it is easier to achieve, each column up to 3 views, up to 3 rows, we use a for loop to get it done.

  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)
        }

    }
Copy the code
Set up data and add child views

When you write the above two methods, you’re 90% done. But we haven’t actually added an ImageView to it yet, so now we expose a method, set the data and add an ImageView

//loadCallback is a callback to load the image. 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() }Copy the code

Finally, open up custom XML attributes, define spacing and so on, to be customizable in XML files.

Results the following

So far, a nine-grid layout has been realized, is not very simple. In fact, whether it is a custom ViewGroup or a custom View, it is important to understand the logic of it before writing the code.