For example, for Compose, which is currently being implemented by Android, Kotlin makes it easier and faster to write UI’s. For example, for Compose, you don’t have to worry about nesting layouts or declarative UI’s.

Today I’m going to share with you a non-traditional custom ViewGroup writing method, let you no longer “fear” of custom ViewGroup, and with the help of Kotlin, we can use native writing method, also can quickly write no nested layout.

Why custom ViewGroups

ConstraintLayout has been reduced a lot since ConstraintLayout was introduced, but is ConstraintLayout the best way to achieve performance? Overall, performance has improved compared to other layouts. ConstraintLayout has too many scenarios to account for, and its logic is complex. For a defined page, a custom ViewGroup that is “targeted” may not achieve maximum performance. ConstraintLayout is better than ConstraintLayout because you are only responsible for one page, not the whole thing.

When I say custom ViewGroup, I mean write a layout in code, not a public control for others to use. Telegram layout is all written in code.

So why don’t we write layouts in code?

  • Customizing viewGroups is too complicated. There are a lot of MeasureSpec cases.
  • I forget every time I write. I have to look it up. I forget when I learn
  • It’s inefficient. I got a little performance boost. No need

In summary, the reason is that customizing viewgroups is difficult and cumbersome. It used to be a hassle to write code in Java, but with Kotlin, it’s now very elegant to write layouts in code. Next, I’m going to walk you through the process of customizing viewGroups, and then implementing them with Kotlin.

What does a custom ViewGroup do

A ViewGroup which steps, I think we all know, is nothing more than measurement, layout, drawing, these three steps, measurement is the child View size measurement, and then calculate their own size; Layout is setting the position of the View at a time; You don’t really need to draw for a ViewGroup, you just draw something on your own. It’s not so hard to look at it that way. What’s so hard? I think it’s because of this chart:

Is the measurement of the various modes, many books about the custom View will give this table, in fact, this is summed up by the author, Android official website is not these things.

Forget about this table for now, and just look at the measurement modes: EXACTLY, AT_MOST, and UNSPECIFIED.

  • EXACTLY what you set is EXACTLY what you set
  • AT_MOST is the maximum that you can use, which is how big the child View is
  • UNSPECIFIED, this is typically measured again, as in the case of the Weight used in the LinearLayout

UNSPECIFIED is rarely used in cases where we code the layout, which leaves only two modes of UNSPECIFIED, which are summarized as UNSPECIFIED and as unambiguous as the View actually is. Light said we estimate that there is no specific concept, next on the code.

How to improve ViewGroup writing efficiency with Kotlin

Next, let’s use Kotlin’s extension method to do what you need to customize a ViewGroup step by step.

MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec

// EXACTLY
fun Int.toExactlyMeasureSpec(a) = MeasureSpec.makeMeasureSpec(this, MeasureSpec.EXACTLY)
// The AT_MOST measurement mode
fun Int.toAtMostMeasureSpec(a) = MeasureSpec.makeMeasureSpec(this, MeasureSpec.AT_MOST)
Copy the code

When we set the width and height of a control, we usually give a specific value, such as MATCH_PARENT or WRAP_CONTENT. With Kotlin we can extend the View directly to get its default width and height:

// Get the default measurement of the View width
fun View.defaultWidthMeasureSpec(parent: ViewGroup): Int {
    return when (layoutParams.width) {
        // If it is MATCH_PARENT, it fills the parent layout, so give it the exact width of the parent layout
        MATCH_PARENT -> parent.measuredWidth.toExactlyMeasureSpec()
        // If it is WRAP_CONTENT, then it should be as big as it can be
        WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec()
        // 0 is indeterminate, we have UI draft here, there is no indeterminate case, so we don't have to worry about it here
        0 -> throw IllegalAccessException("I don't think about it$this")
        // The final value is the specific value, so I give you the specific value
        else -> layoutParams.width.toExactlyMeasureSpec()
    }
}
// Get the default measurement of the height of the View, the same principle used to get the width above
fun View.defaultHeightMeasureSpec(parent: ViewGroup): Int {
    return when (layoutParams.height) {
        MATCH_PARENT -> parent.measuredHeight.toExactlyMeasureSpec()
        WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec()
        0 -> throw IllegalAccessException("I don't think about it$this")
        else -> layoutParams.height.toExactlyMeasureSpec()
    }
}
Copy the code

With this in mind, it’s much easier to write a custom ViewGroup. We can measure a control and just write:

textView.measure(textView.defaultWidthMeasureSpec(this), textView.defaultHeightMeasureSpec(this))
Copy the code

Wait, this is still a bit complicated to write, so why don’t we just define an extension method and let the View measure directly by default:

fun View.autoMeasure(parent: ViewGroup) {
    measure(
        this.defaultWidthMeasureSpec(parent),
        this.defaultHeightMeasureSpec(parent)
    )
}
Copy the code

The next time you use it, you can write:

textView.autoMeasure(this)
Copy the code

Is it more simple, to this, the basic code of measurement is almost finished, by the way, the basic method of layout also write it, layout is relatively simple, is to tell the location of the child View.

// Set the view position
fun View.autoLayout(parent: ViewGroup, x: Int = 0, y: Int = 0, fromRight: Boolean = false) {
    // Determine if the layout starts from the right
    if(! fromRight) {// Note why measuredWidth is used instead of width
        // Since width is evaluated by mright-mleft, neither of them is assigned, so both are 0
        layout(x, y, x + measuredWidth, y + measuredHeight)
    } else {
        autoLayout(parent.measuredWidth - x - measuredWidth, y)
    }
}
Copy the code

We can write all of these methods into a class, write custom ViewGroup, and inherit from it, like this:

 // To make it easier to set dp sp, we declare the extended properties directly here
val Int.dp
    get() = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), 
        Resources.getSystem().displayMetrics
    ).toInt()
val Float.sp
    get() = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_SP, this,
        Resources.getSystem().displayMetrics
    )

abstract class CustomViewGroup(context: Context) : ViewGroup(context) {

    // Easy to get width and height with Margin
    protected val View.measuredWidthWithMargins get() = measuredWidth + marginStart + marginEnd
    protected val View.measuredHeightWithMargins get() = measuredHeight + marginTop + marginBottom

    protected fun Int.toExactlyMeasureSpec(a) = MeasureSpec.makeMeasureSpec(this, MeasureSpec.EXACTLY)
  
    protected fun Int.toAtMostMeasureSpec(a) = MeasureSpec.makeMeasureSpec(this, MeasureSpec.AT_MOST)

    protected fun View.defaultWidthMeasureSpec(parent: ViewGroup): Int {
        return when (layoutParams.width) {
            MATCH_PARENT -> parent.measuredWidth.toExactlyMeasureSpec()
            WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec()
            0 -> throw IllegalAccessException("I don't think about it$this")
            else -> layoutParams.width.toExactlyMeasureSpec()
        }
    }

    protected fun View.defaultHeightMeasureSpec(parent: ViewGroup): Int {
        return when (layoutParams.height) {
            MATCH_PARENT -> parent.measuredHeight.toExactlyMeasureSpec()
            WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec()
            0 -> throw IllegalAccessException("I don't think about it$this")
            else -> layoutParams.height.toExactlyMeasureSpec()
        }
    }

    protected fun View.autoMeasure(a) {
        measure(
            this.defaultWidthMeasureSpec(this@CustomViewGroup),
            this.defaultHeightMeasureSpec(this@CustomViewGroup))}protected fun View.autoLayout(x: Int = 0, y: Int = 0, fromRight: Boolean = false) {
        if(! fromRight) { layout(x, y, x + measuredWidth, y + measuredHeight) }else {
            autoLayout(this@CustomViewGroup.measuredWidth - x - measuredWidth, y)
        }
    }
}
Copy the code

Try writing a custom ViewGroup

Take the calculator interface for example. The above 👆 is implemented by ConstraintLayout. See what controls there are: 1 EditText, 17 buttons. Let’s try a simple duplicate with a custom ViewGroup and go straight to the code:

class CalculatorLayout(context: Context) : CustomViewGroup(context) {
    // We can just new the control in this way and set some properties, which also eliminates the need for findViewById and does not worry about null Pointers
    val etResult = AppCompatEditText(context).apply {
        typeface = ResourcesCompat.getFont(context, R.font.comfortaa_regular)
        setTextColor(ResourcesCompat.getColor(resources, R.color.white, null))
        background = null
        textSize = 65f
        gravity = Gravity.BOTTOM or Gravity.END
        maxLines = 1
        isFocusable = false
        isCursorVisible = false
        setPadding(16.dp, paddingTop, 16.dp, paddingBottom)
        layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
        // Note that direct add does not trigger onMeasure
        addView(this)}// Background behind numeric keypad
    valkeyboardBackgroundView = View(context).apply {... }// Pull out a button of the same style
    class NumButton(context: Context, text: String, parent: ViewGroup) : AppCompatTextView(context) {
        init {
            setText(text)
            gravity = Gravity.CENTER
            background =
                ResourcesCompat.getDrawable(resources, R.drawable.ripple_cal_btn_num, null)
            layoutParams =
                MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
                    leftMargin = 2.dp
                    rightMargin = 2.dp
                    topMargin = 6.dp
                    bottomMargin = 6.dp
                }
            isClickable = true
            setTextAppearance(context, R.style.StyleCalBtn)
            parent.addView(this)}}// Specify the array button
    val btn0 = NumButton(context, "0".this)...init {
        // Create a background for yourself
        background = ResourcesCompat.getDrawable(resources, R.drawable.shape_cal_bg, null)}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // Calculate the size of the numeric button
        val allSize =
            measuredWidth - keyboardBackgroundView.paddingLeft - keyboardBackgroundView.paddingRight -
                    btn0.marginLeft * 8
        val numBtSize = (allSize * (1 / 3.8)).toInt()
        // Calculate the size of the action button
        val operatorBtWidth = (allSize * (0.8 / 3.8)).toInt()
        val operatorBtHeight = (numBtSize * 4 + btn0.marginTop * 6 - btnDel.marginTop * 8) / 5

        // Calculate the height of the digital disk
        val keyboardHeight =
            keyboardBackgroundView.paddingTop + keyboardBackgroundView.paddingBottom +
                    numBtSize * 4 + btn0.marginTop * 8

        // Finally give the height remaining space to EditText
        val editTextHeight = measuredHeight - keyboardHeight

        // Measure the background
        keyboardBackgroundView.measure(
            measuredWidth.toExactlyMeasureSpec(),
            keyboardHeight.toExactlyMeasureSpec()
        )

        // Measure button
        btn0.measure(numBtSize.toExactlyMeasureSpec(), numBtSize.toExactlyMeasureSpec())
        ...
        btnDiv.measure(operatorBtWidth.toExactlyMeasureSpec(), operatorBtHeight.toExactlyMeasureSpec())
        ...

        / / the EditText measurement
        etResult.measure(
            measuredWidth.toExactlyMeasureSpec(),
            editTextHeight.toExactlyMeasureSpec()
        )

        // Finally set your own width and height
        setMeasuredDimension(measuredWidth, measuredHeight)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // All the measurements are finished, let's put them one by one
        / / put the EditText first
        etResult.autoLayout()

        // Put the background on
        keyboardBackgroundView.autoLayout(0, etResult.bottom)

        // Start putting the button
        btn7.let {
            it.autoLayout(
                keyboardBackgroundView.paddingLeft + it.marginLeft,
                keyboardBackgroundView.top + keyboardBackgroundView.paddingTop + it.marginTop
            )
        }
        btn8.let {
            it.autoLayout(
                btn7.right + btn7.marginRight + it.marginLeft,
                btn7.top
            )
        }
        btn9.let {
            it.autoLayout(
                btn8.right + btn8.marginRight + it.marginLeft,
                btn7.top
            )
        }
        ...
    }
}
Copy the code

Ok, that completes the custom ViewGroup, and we can use it like this:

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)
    val contentView = CalculatorLayout(this)
    setContentView(contentView)
    
    // No findViewById, KTX plugin, ViewBinding, etc
    contentView.btnDel.setOnClickListener {
        contentView.etResult.setText("")}}Copy the code

Take a look at the final comparison:

It’s not too complicated to write a layout in Kotlin code. The downside is that you can’t preview it in Android Studio.

The end of the

Although it is a bit more difficult to write than XML, I feel that it is almost the same after mastering it. It also helps us to be more familiar with the custom View piece. Interested partners can try this method first when the project is not busy. Of course, you can also try Compose, which is ultimately a ViewGroup, and look at AndroidComposeView, which is also eventually added to the DecorView.

Finally, thanks to drakeet, the author of Pure Writing, for sharing.

Demo address: github.com/IAn2018cs/C…