The traditional way of writing interfaces

Suppose we want to complete a layout similar to the one shown

The way most people do it is to write linear layouts versus layouts in XML

This layout nesting is actually a bit deep, is there a better way to write it?

The answer is yes, we can use constraint layout

Constraint layout to complete the interface naturally does not need layout nesting, a layer of viewGroup solved.

Are there any other plans? Or what are the limitations of constrained layouts?

  1. Since constrained layout is used it is inevitable to read the XML once involving file IO
  2. Parsing an XML file itself is time-consuming
  3. The performance of the constrained layout is not as good as expected because the layout takes care of too many scenarios, resulting in extremely complex logic

Is there a better solution?

The answer is yes. In the ancient days of Android, there was a solution called X2C

What is this?

It’s basically converting XML into code, and didn’t we write the layout in XML before? Why don’t we just write the layout in code?

This eliminates the need to read and parse the XML.

So why hasn’t it caught on?

Because code is too cumbersome to write layouts, previously only the Java language. The ROI is too low.

But now that we have Kotlin anything is possible.

The third point of the final flaw is that the performance of constrained layouts is not that good. Can our custom viewGroup perform better than Google’s god?

Your custom viewGroup, 99 percent of the time, is not going to be better than Google, right here, it’s not going to be better, but our viewGroup is just going to take care of our UI design, we don’t have to write so much logic to take care of all the situations and given this particular condition, The performance of our viewGroup can definitely exceed the constraint layout

The biggest obstacle to writing a viewGroup by hand

Many people find it difficult to customize viewgroups, is that the difficulty?

Look at this picture and you’re confused.

As far as I know, you don’t have to care about Unspecified, but rest assured you won’t have this scenario in your career. If you do, you should still be at Google. And then what’s left is exactly and at most. It just translates

Use as much as you actually use and as much as you can. 六四屠杀

Let’s use kotlin code to translate:

0 就是 unspecified

fun Int.toExactlyMeasureSpec(): Int { return MeasureSpec.makeMeasureSpec(this, MeasureSpec.EXACTLY) } fun Int.toAtMostMeasureSpec(): Int { return MeasureSpec.makeMeasureSpec(this, MeasureSpec.AT_MOST) } fun View.defaultWidthMeasureSpec(parentView: ViewGroup): Int { return when (layoutParams.width) { MATCH_PARENT -> parentView.measuredWidth.toExactlyMeasureSpec() WRAP_CONTENT ->  WRAP_CONTENT.toAtMostMeasureSpec() 0 -> throw IllegalAccessException("need special treatment for $this") else -> layoutParams.width.toExactlyMeasureSpec() } } fun View.defaultHeightMeasureSpec(parentView: ViewGroup): Int { return when (layoutParams.height) { MATCH_PARENT -> parentView.measuredHeight.toExactlyMeasureSpec() WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec() 0 -> throw IllegalAccessException("need special treatment for $this") else -> layoutParams.height.toExactlyMeasureSpec() } }Copy the code

It’s that simple.

Based on this, we can customize a series of convenient extension functions as a base class to facilitate the customization of the view later

abstract class CustomLayout(context: Context) : ViewGroup (context) {/ / automatic measure fun the autoMeasure () {measure (enclosing defaultWidthMeasureSpec (this @ CustomLayout), This. DefaultHeightMeasureSpec (this @ CustomLayout)} / / automatic layout, some view from the right to put, this time to see the else inside logic can be fun the layout (x: Int = 0, y: Int = 0, fromRight: Boolean = false) { if (! fromRight) { layout(x, y, x + measuredWidth, Y + measuredHeight)} else {layout ([email protected] - x - measuredWidth, y)}} / / dp px val Int. Dp: Int the get () = (this * resources. DisplayMetrics. Density + 0.5 f), toInt () fun Int. J toExactlyMeasureSpec () : Int { return MeasureSpec.makeMeasureSpec(this, MeasureSpec.EXACTLY) } fun Int.toAtMostMeasureSpec(): Int { return MeasureSpec.makeMeasureSpec(this, MeasureSpec.AT_MOST) } fun View.defaultWidthMeasureSpec(parentView: ViewGroup): Int { return when (layoutParams.width) { MATCH_PARENT -> parentView.measuredWidth.toExactlyMeasureSpec() WRAP_CONTENT ->  WRAP_CONTENT.toAtMostMeasureSpec() 0 -> throw IllegalAccessException("need special treatment for $this") else -> layoutParams.width.toExactlyMeasureSpec() } } fun View.defaultHeightMeasureSpec(parentView: ViewGroup): Int { return when (layoutParams.height) { MATCH_PARENT -> parentView.measuredHeight.toExactlyMeasureSpec() WRAP_CONTENT -> WRAP_CONTENT.toAtMostMeasureSpec() 0 -> throw IllegalAccessException("need special treatment for $this") else -> LayoutParams. Height. ToExactlyMeasureSpec ()}} / / the view about twice the margin also bring together val the measuredWidthWithMargins the get () = measuredWidth + marginLeft + marginRight val View.measuredHeightWithMargins get() = measuredHeight + marginTop + Class LayoutParams(width: Int, height: Int) : MarginLayoutParams(width, height)}Copy the code

Actually start customizing viewgroups

Let’s review the diagram before first, that is the effect diagram, we first analyze the effect diagram to achieve the main points in a few steps.

  1. You define the elements like the largest base image, and then the buttons underneath the base image and then the avatar, the nickname, and the description

There are five views

  1. Measure these 5 views respectively

  2. Conduct layout for these five views respectively

Easy, just three steps

Let’s start with the first step. The first step is the easiest. It’s just a matter of setting some properties for the view, such as width, height, image source

It is the second step that is the most difficult. Measure is the most difficult process. If we solve it in the third step, we just put the view according to the result of the second step

How do we write the process of measure according to the information in the picture

  1. Base picture: The simplest setting is a fixed width and height. So the width is match height and I’ll just write px for you
  2. The button at the bottom right of the bottom image: same as 1, it’s easy to use our autoMeasure method as long as you set its width and height
  3. Avatars are also handy, which autoMeasure can solve

Here’s the thing: nicknames and descriptions are a little harder. Where is difficult? Since the right edge of the nickname and description should be on the left side of the button, you can’t go beyond the left side of the button

What is the width of the nickname and description? If you look at the image carefully: their width is the width of the parent layout – the width of the avatar – the width of the button – they are away from the marginLeft of the left avatar and that’s how wide they are 2 can be used.

Now we have the width and then we have the height. In fact, the height of this description can determine the height of the entire viewgroup. Why?

Because if the textView described is taller than the avatar then the height of the entire viewgroup should be the height of the base image plus the height of the nickname and description plus the marginTop of the avatar

If it’s not higher than the head, it’s the height of the head plus the height of the base

Figure that out so measure’s process has some ideas. The rest of the layout process is simpler. All you have to do is arrange the views in order according to the value of your measure

Last code:

class TheLayout2(context: Context, attributes: AttributeSet) : CustomLayout(context) { val header = AppCompatImageView(context).apply { scaleType = ImageView.ScaleType.CENTER_CROP setImageResource(R.mipmap.tlyhp) layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, AddView (this)} val fab = if the view is attached to a view tree, measure-layout-draw is used FloatingActionButton(context).apply { setImageResource(R.mipmap.ic_launcher) layoutParams = LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup. LayoutParams. WRAP_CONTENT). Also {it. RightMargin = 12. Dp} addView (this)} / / avatar val avatarIv = AppCompatImageView(context).apply { scaleType = ImageView.ScaleType.CENTER_CROP setImageResource(R.mipmap.abc) layoutParams = LayoutParams(50.dp, TopMargin = 12.dp it.topmargin = 12.dp} addView(this)} // nickname val nameTV = AppCompatTextView(context).apply {textSize = 3.dp.toFloat() text = "11112312311211" isSingleLine = true layoutParams = LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup. LayoutParams. WRAP_CONTENT). Also {it. LeftMargin = 12. Dp} addView (this)} / / describe val desTV = AppCompatTextView(context).apply {textSize = 5.dp.tofloat () text = "I am gorgeous" //text = "I am gorgeous" "I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk I'm a hunk" ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ).also { it.leftMargin = 12.dp it.topMargin = 12.dp } addView(this) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, AutoMeasure () // Calculate the width of the nickname val nameTvWidth = MeasuredWidth - avatarIv. MeasuredWidthWithMargins - fab. MeasuredWidthWithMargins - nameTV. MarginLeft / / computing describe the width of the val desTvWidth = measuredWidth - avatarIv.measuredWidthWithMargins - fab.measuredWidthWithMargins - desTV.marginLeft nameTV.measure(nameTvWidth.toExactlyMeasureSpec(), nameTV.defaultHeightMeasureSpec(this)) desTV.measure(desTvWidth.toExactlyMeasureSpec(), desTV.defaultHeightMeasureSpec(this)) val contentHeight = (avatarIv.marginTop + desTV.measuredHeightWithMargins + nameTV.measuredHeightWithMargins).coerceAtLeast( avatarIv.measuredHeightWithMargins ) setMeasuredDimension(measuredWidth, header.measuredHeight + contentHeight) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { header.layout(0, 0) fab.let { it.layout( x = it.marginRight, y = header.bottom - (it.measuredHeight / 2), fromRight = true ) } avatarIv.let { it.layout(x = it.marginLeft, y = header.bottom + it.marginTop) } nameTV.let { it.layout(x = avatarIv.right + it.marginLeft, y = avatarIv.top) } desTV.let { it.layout(x = avatarIv.right + it.marginLeft, y = nameTV.bottom + it.marginTop) } } }Copy the code

Then run, flawlessly, to see that the height of the viewGroup changes as we change the des contents.

In fact, you can see that there are no overlapping areas at all, so it’s perfect.

In addition to the advantages mentioned above, the other advantage of findviewbyId is that you don’t need findviewbyId, and we all know that findviewbyId has a certain performance cost when you have a complex layout, so that’s gone now.

You can just go to the viewGroup and get the child view inside of it

conclusion

Using Kotlin to customize viewgroups is not complicated. Compared to XML’s traditional view approach, handwritten viewgroups have higher performance and cost compared to XML, but they are generally acceptable.

Finally, thanks to God Drakeet for sharing.