Related reading: Try implementing the Android View Touch event distribution process yourself

Android View layout with Viewrotimpl as the starting point, open the layout process of the entire View tree, and the layout process itself is divided into two parts: measure and layout. The hierarchical recursive layout of the View tree itself determines the position of the View in the interface.

Here is an attempt to implement this mechanism yourself with minimal code. Note that the following classes are custom classes and do not use the same class in the Android source code.

MeasureSpec

MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec = MeasureSpec;

class MeasureSpec(var mode: Int = UNSPECIFIED, var size: Int = 0) {
    companion object {
        const val UNSPECIFIED = 0
        const val EXACTLY = 1
        const val AT_MOST = 2}}Copy the code

There are also three modes, which indicate that the parent layout has no limit on the child layout, the parent layout has a fixed value on the child layout, and the parent layout has a maximum limit on the child layout.

LayoutParam

LayoutParam is defined in the source code inside various viewgroups. LayoutParam is a static inner class, which is used in the child View of the ViewGroup layout. Here we define it as the top class, and contains only two properties, width and height. Corresponds to the layout_width and layout_height attributes in the XML file. Also define MATCH_PARENT and WRAP_CONTENT.

class LayoutParam(var width: Int.var height: Int) {
    companion object {
        const val MATCH_PARENT = -1
        const val WRAP_CONTENT = -2}}Copy the code

So let’s implement View and ViewGroup.

View

(1) we define the View coordinates, and the source consistent, here is relative to the parent View coordinates, and the previous View related articles try to write their own Android View Touch event distribution is different, that View coordinates are absolute coordinates.

(2) defines the padding, (3) represents the measurement width and height of the measure process, and (4) is the layoutParam specified in the layout file

These attributes, in summary, are (2) (4) specified by the developer in the layout, (3) measured by the View itself through the measurement process, (1) determined by the layout process, which is our purpose, including (3) the meaning of existence is also to determine the values in (4).

The following began to write the measurement process, although these codes are rewritten, a lot of simplification, but the overall process is still consistent with the source code, can more clearly understand the Android View tree layout is how to achieve.

(5) onMeasure directly calls onMeasure to start the measurement process, while onMeasure simply sets the limit value in the parent ViewGroup of MeasureSpec as the measurement value to end its own measurement process (6), because onMeasure needs to be inherited and used. Different views measure differently, so it’s easy to deal with here.

(7) to start the layout process, first call setFrame method to save coordinates (8), and call onLayout callback, here is empty implementation (9).

At this point, the View layout related methods are completed.

open class View {
    open var tag = javaClass.simpleName

    var left = 0
    var right = 0
    var top = 0
    var bottom = 0/ / 1

    var paddingLeft = 0
    var paddingRight = 0
    var paddingTop = 0
    var paddingBottom = 0/ / 2

    var measuredWidth = 0
    var measuredHeight = 0/ / 3

    var layoutParam = LayoutParam(
        LayoutParam.WRAP_CONTENT,
        LayoutParam.WRAP_CONTENT
    )/ / 4

    fun measure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {
        onMeasure(widthMeasureSpec, heightMeasureSpec)
    }/ / 5

    open fun onMeasure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {
        setMeasuredDimension(widthMeasureSpec.size, heightMeasureSpec.size)/ / 6
    }

    fun setMeasuredDimension(measuredWidth: Int, measuredHeight: Int) {
        this.measuredWidth = measuredWidth
        this.measuredHeight = measuredHeight
    }

    fun layout(l: Int, t: Int, r: Int, b: Int) {
        val changed = setFrame(l, t, r, b)/ / 8
        onLayout(changed, l, t, r, b)
    }/ / 7

    private fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
        var changed = false
        if(l ! = left || t ! = top || r ! = right || b ! = bottom) { left = l top = t right = r bottom = b changed =true
        }
        println("$tag = L: $l, T: $t, R: $r, B: $b")
        return changed
    }

    open fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}/ / 9

    fun resolveSize(size: Int, measureSpec: MeasureSpec): Int {
        return when (measureSpec.mode) {
            MeasureSpec.EXACTLY -> measureSpec.size
            MeasureSpec.AT_MOST -> minOf(size, measureSpec.size)
            else -> size
        }
    }/ / 10
}
Copy the code

ViewGroup

The empty implementation of onLayout in the View is declared abstract, requiring subclasses to implement the layout algorithm themselves. The ViewGroup itself is not allowed to be used as a layout.

abstract class ViewGroup(vararg val children: View) : View() {
    abstract override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int)
}
Copy the code

In this way, the skeleton of the entire Android View hierarchy has been built, in the source code, for the layout of the View, the main thing is done so much. Other various views and viewgroups are inherited to realize their own measurement algorithm (that is, the sub-view implements onMeasure) and layout algorithm (that is, the sub-viewgroup implements onMeasure and onLayout).

Now we rely on this framework to implement a View and a ViewGroup.

Text

Here we implement a TextView, because we only want to illustrate the principle of View measurement, so we only support two properties text and textSize.

Just to implement onMeasure, add the left and right paddings, multiply the length of the string by the size as width (1), add the top and bottom paddings, and add the size as height.

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

ResolveSize is defined at (10) of View section, in which the processing logic is that, when the limit is fixed, the measured value is the limit value; when the limit is upper, the measured value is the limit value and the ideal value is smaller; when the limit is unlimited, the ideal value is taken.

In this way, the whole TextView measurement process is completed. For layout procedures, onLayout is left empty and does not need to be overwritten, since the layout method already has its own coordinates.

class Text(private val text: String, private val textSize: Int = 10) : View() {
    override var tag: String = "Text($text)"

    override fun onMeasure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {
        val width = paddingLeft + paddingRight + text.length * textSize/ / 1
        val height = paddingTop + paddingBottom + textSize
        setMeasuredDimension(
            resolveSize(width, widthMeasureSpec),/ / 2
            resolveSize(height, heightMeasureSpec)
        )
    }
}
Copy the code

Column

The following defines a LinearLayout similar to vertical orientation to illustrate the layout process of the ViewGroup.

For the LinearLayout in the source code, the layout property starting with layout_ used in the sub-layout corresponds to the Inner class of LayoutParams in the LinearLayout. Here we directly use the LayoutParams defined above. Some functions of the LinearLayout are not implemented, such as layout_margin, layout_weight, and layout_gravity.

In onMeasure, there are two things to do. The first thing is to measure itself as the parent View does, by calling setMeasuredDimension. The second thing is to start measuring each of the subviews, and in fact, the second thing is itself a prerequisite for the first thing, because if you don’t finish measuring the subviews, you can’t determine their length or width at all.

ChildWidthMeasureSpec, childHeightMeasureSpec, childWidthMeasureSpec, childHeightMeasureSpec, Here the length and width limits are determined by the getChildMeasureSpec method (2), which is defined in the ViewGroup in the source code.

(3) The method receives three parameters. Spec is the limit of the parent View for Column itself, padding is the size of the Column that has been used up by the time the View is measured (Column is arranged side by side, so this value must be needed), ChildDimension is the layout_width or layout_height specified by the developer in the layout file.

As a result, the spec has UNSPECIFIED, EXACTLY, and AT_MOST types, while the childDimension has MATCH_PARENT, WRAP_CONTENT, and Exact values. These intersections need to be considered separately. In the source code, put the spec in the outer layer, and the childDimension in the inner layer. Here we put the childDimension in the outer layer (4), and the spec in the inner layer for a cleaner implementation.

(5) When the childDimension is MATCH_PARENT, as long as the limiting mode is faithfully passed, the size is the remaining size calculated at (6).

As with WRAP_CONTENT, the mode is limited to AT_MOST, and the remaining size as calculated in (6) is still UNSPECIFIED, but the spec.mode is UNSPECIFIED, which is passed along to (7).

(8) Finally, in the case of the exact values specified by the developer by childDimension, just pass them as they are, regardless of the parent layout restrictions.

In this way, the limits passed to the respective views at (1) are obtained, and the measurement of the sub-views is started. After the measurement of the currently traversed sub-views is completed, the measured height of the sub-views is needed to update the used height value (9). Since Column is arranged vertically in a single row, the usedWidth does not need to be updated. But you need to update the width value to be the desired width for the Column itself.

(10) Once the traversal is complete, pass the return value of resolveSize to the setMeasuredDimension as in the previous section Text, and the Column measurement process is completed.

class Column(vararg children: View) : ViewGroup(*children) {
    override fun onMeasure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {
        var usedHeight = paddingTop + paddingBottom
        val usedWidth = paddingLeft + paddingRight
        var width = 0
        children.forEach { child ->
            val childWidthMeasureSpec =
                getChildMeasureSpec(widthMeasureSpec, usedWidth, child.layoutParam.width)
            val childHeightMeasureSpec =
                getChildMeasureSpec(heightMeasureSpec, usedHeight, child.layoutParam.height)
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec)/ / 1
            usedHeight += child.measuredHeight/ / 9
            width = maxOf(width, child.measuredWidth)
        }
        setMeasuredDimension(
            resolveSize(width, widthMeasureSpec),
            resolveSize(usedHeight, heightMeasureSpec)
        )/ / 10
    }

    private fun getChildMeasureSpec(
        spec: MeasureSpec,
        padding: Int,
        childDimension: Int
    ): MeasureSpec {/ / 3
        val childWidthSpec = MeasureSpec()
        val size = spec.size - padding/ / 6
        when (childDimension) {/ / 4
            LayoutParam.MATCH_PARENT -> {
                childWidthSpec.mode = spec.mode
                childWidthSpec.size = size
            }/ / 5
            LayoutParam.WRAP_CONTENT -> {
                if (spec.mode == MeasureSpec.AT_MOST || spec.mode == MeasureSpec.EXACTLY) {
                    childWidthSpec.mode = MeasureSpec.AT_MOST
                    childWidthSpec.size = size
                } else if (spec.mode == MeasureSpec.UNSPECIFIED) {
                    childWidthSpec.mode = MeasureSpec.UNSPECIFIED
                    childWidthSpec.size = 0/ / 7}}else -> {
                childWidthSpec.mode = MeasureSpec.EXACTLY
                childWidthSpec.size = childDimension/ / 8}}return childWidthSpec
    }/ / 2

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var childTop = paddingTop
        children.forEach { child ->
            child.layout(
                paddingLeft,
                childTop,
                paddingLeft + child.measuredWidth,
                childTop + child.measuredHeight
            )
            childTop += child.measuredHeight
        }
    }
}
Copy the code

As for the onLayout method, since we already know the measurement width and height of each sub-view, we only need to traverse each sub-view and set the coordinate one by one. The coordinate setting of Column itself has been realized in the Layout method of View.

So the entire Android-like layout is rewritten.

use

Let’s verify our code:

fun main(a) {
    val page = Column(
        Text("Marshmallow").apply {
            layoutParam = LayoutParam(
                LayoutParam.WRAP_CONTENT,
                LayoutParam.WRAP_CONTENT
            )
        },
        Text("Nougat").apply {
            layoutParam = LayoutParam(
                LayoutParam.WRAP_CONTENT,
                LayoutParam.WRAP_CONTENT
            )
        },
        Text("Oreo").apply {
            layoutParam = LayoutParam(
                LayoutParam.WRAP_CONTENT,
                LayoutParam.WRAP_CONTENT
            )
            paddingTop = 10
            paddingBottom = 10
        },
        Text("Pie").apply {
            layoutParam = LayoutParam(
                LayoutParam.WRAP_CONTENT,
                LayoutParam.WRAP_CONTENT
            )
        }
    ).apply {
        layoutParam = LayoutParam(
            LayoutParam.WRAP_CONTENT,
            LayoutParam.WRAP_CONTENT
        )
        paddingLeft = 10
        paddingRight = 10
        paddingBottom = 10
    }/ / 1

    val root = Column(page)/ / 2
    root.measure(MeasureSpec(MeasureSpec.AT_MOST, 1080), MeasureSpec(MeasureSpec.AT_MOST, 1920))
    root.layout(0.0.1080.1920)/ / 3
}
Copy the code

(1) Define a layout page, just like the layout file written in Android, except that this is more like the declarative UI of Flutter.

In the source code, the layout process can simply be thought of as initiated in ViewRootImpl, with performMeasure inside. PerformLayout starts the layout process from a DecorView, and Column at (2) looks like a DecorView. The next two lines are similar to the layout flow initiated by the perform method in ViewRootImpl (we won’t consider the draw part here because it’s irrelevant).

Run view print, as expected.

Column = L: 0, T: 0, R: 1080, B: 1920
Column = L: 0, T: 0, R: 110, B: 70
Text(Marshmallow) = L: 10, T: 0, R: 120, B: 10
Text(Nougat) = L: 10, T: 10, R: 70, B: 20
Text(Oreo) = L: 10, T: 20, R: 50, B: 50
Text(Pie) = L: 10, T: 50, R: 40, B: 60
Copy the code

conclusion

  1. The framework code of the whole View and ViewGroup on the layout (including measure, layout) is very simple, and the specific layout algorithm needs to be realized by each subclass.

  2. ViewGroup traversal of subviews, which needs to be overridden, happens in methods that start with on. The measurement width of the parent View itself requires the measurement width of the child View, so setMeasuredDimension is called after the onMeasure traversal. The parent View coordinates do not need to care about the child View, so it is set in the layout method like View, before onLayout traverses the child View.

  3. MeasuredWidth measuredHeight Is measured by matching the desired size (width, height in the code) of the View to the measured size (measuredWidth, height).

  4. The basic purpose of the whole layout process is to determine the four coordinate values in the View, which are set in the layout method. Therefore, the call to the layout method determines the result of the layout process, and measure can be said to be an auxiliary to this process.