Related readings:Try to implement the Android View Touch event distribution process yourself
The layout of Android View starts with ViewRootImpl, which starts the layout process of the entire View tree. The layout process itself is divided into two parts: measurement and layout. It is recursively laid out with the hierarchy of the View tree itself to determine the position of the View in the interface.
Let’s try to implement this mechanism by yourself with the least code. Note that the following classes are custom classes and do not use the same name class in the Android source code.
MeasureSpec
First, define MeasureSpec, which is a class that describes the parent layout constraints on the child layout. In the Android source code, it is an int value, and the mode and size are obtained through bit operations. Here we implement it as a class for convenience:
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 } }
It also contains three modes, which respectively indicate that the parent layout has no restrictions on the child layout, the parent layout requires a fixed value for 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. It is a static inner class for use in subviews in the ViewGroup layout. Here we define it as a top-level class and only contains two attributes: width and height, corresponding to the layout_width and layout_height attributes in the xml file. MATCH_PARENT and WRAP_CONTENT are also defined.
class LayoutParam(var width: Int, var height: Int) { companion object { const val MATCH_PARENT = -1 const val WRAP_CONTENT = -2 } }
Next we implement View and ViewGroup.
View
(1) The coordinates of the View we define are consistent with the source code. Here we represent the coordinates relative to the parent View, andPrevious article related to View Try writing on your own Android View Touch event distribution, the coordinates of that article's View are absolute coordinates.
(2) The padding is defined, (3) The measurement width and height of the measurement process are represented, and (4) The layoutParam specified in the layout file is specified.
These attributes are summarized as (2) (4) specified by the developer in the layout, (3) measured by the View itself through the measurement process, (1) finalized through the layout process, that is, our purpose, including (3) the meaning of existence is also to determine the value in (4).
Let’s start writing the measurement process. Although these codes are rewritten and have been simplified a lot, the overall process is still consistent with the source code, and we can understand more clearly how the layout of the Android View Tree is implemented.
(5) The measure directly calls onMeasure to start the measurement process, and onMeasure is simply set to the limit value in the parent ViewGroup in MeasureSpec as the measurement value, which ends its own measurement process (6). Because onMeasure needs to be inherited and used, the measurement methods of different Views are not the same, so it is simply processed here.
(7) Start the layout process, first call the setFrame method to save the coordinates (8), and call the onLayout callback, which is empty implementation (9).
At this point, the relevant methods of the View layout have been implemented.
open class View { open var tag = 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(, )//6 } fun setMeasuredDimension(measuredWidth: Int, measuredHeight: Int) { = measuredWidth = 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.AT_MOST -> minOf(size, ) else -> size } }//10 }
ViewGroup
Next, we implement ViewGroup, which has only one abstract method, that is, declare the empty implementation of onLayout in the View as abstract, that is, the subclass requires that the layout algorithm be implemented by itself, and ViewGroup itself is not allowed to be used as layout.
abstract class ViewGroup(vararg val children: View) : View() { abstract override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) }
In this way, the skeleton of the entire Android View hierarchy has been built. In the source code, this is mainly done in terms of the layout of the View. Various other Views and ViewGroups implement their respective measurement algorithms (i.e. subView implements onMeasure) and layout algorithms (i.e. subViewGroup implements onMeasure and onLayout).
Next, we rely on this framework to implement one View and ViewGroup.
Text
Next, we implement a TextView. Here, because we are just to illustrate the principle of View measurement, we only support two attributes text and textSize.
Just implement onMeasure, add the left and right paddings, add the product of the string length and font size as width (1), add the upper and lower paddings, and add the font size as height. Of course, here we are just simply calculating the diagram like this, and actually calculating the length and width of the TextView must not be calculated like this.
The length and width calculated in this way are the ideal length and width of Text itself, but it also requires the limitation of the parent layout, that is, MeasureSpec, which calls resolveSize, and pass the limit and ideal value in (2).
resolveSize is defined at (10) of the View section, and the processing logic is that when the limit is a fixed value, the measured value is the limit value, and when the limit is the upper limit, the measured value is the limit value and the ideal value is the smallest, and when the limit is unlimited, the ideal value is taken.
In this way, the entire TextView measurement process is completed. For the layout process, since the layout method has already set its own coordinates, it is enough to keep it empty and does not require rewriting.
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 + * textSize//1 val height = paddingTop + paddingBottom + textSize setMeasuredDimension( resolveSize(width, widthMeasureSpec),//2 resolveSize(height, heightMeasureSpec) ) } }
Column
The following defines a LinearLayout similar to the orientation as vertical to illustrate the layout process of the ViewGroup.
For LinearLayout in the source code, the layout attribute starting with layout_ used in the sub-layout corresponds to LayoutParams in the inner class of LinearLayout. Here we directly use the LayoutParams defined above, which is equivalent to some functions in LinearLayout that have not been implemented, such as layout_margin, layout_weight, layout_gravity, which we simply deal with.
In onMeasure, there are two things to do. The first thing is to measure your length and width like the parent View, that is, you need to call setMeasuredDimension; the second thing is to start their measurement for each child View. In fact, the second thing itself is the premise of the first thing, because if the measurement of the child View does not end, your length and width cannot be determined at all.
(1) Calling the child View's measure in the loop starts their measurement process, but it needs to be passed to them for limits, that is, childWidthMeasureSpec and childHeightMeasureSpec. Here, the limits of length and width are determined by the getChildMeasureSpec method (2). This method is defined in the ViewGroup in the source code.
(3) At this method receives 3 parameters, spec is the limit of the parent View of Column itself, padding is the size of the Column used up when measuring the View (because Column wants to arrange the View one by one, this value must be required), and childDimension is the layout_width or layout_height value specified by the developer in the layout file.
Therefore, spec has three types: UNSPECIFIED, EXACTLY, and AT_MOST, and childDimension has three types: MATCH_PARENT, WRAP_CONTENT and exact value. These interleaving situations need to be considered separately. In the source code, place the spec on the outer layer and the childDimension on the inner layer. Here we place the childDimension on the outer layer (4), and the spec on the inner layer to achieve a more concise implementation.
(5) When the childDimension is MATCH_PARENT, as long as the limit mode is passed faithfully, the size is used to use the remaining size calculated at (6).
(6) When the childDimension is WRAP_CONTENT, the mode needs to be set to AT_MOST, and the remaining size calculated at (6) is also used, but it needs to be considered as UNSPECIFIED, and this non-restriction needs to be passed (7).
(8) Finally, corresponding to the case where childrenDimension specifies exact values for developers, as long as the developers specify values are truthfully passed, there is no need to consider the parent layout limitation.
In this way, the limit passed to the respective Views at (1) is obtained, and the measurement of the subview starts. After the measurement of the currently traversed subview is completed, the measured subview height needs to be obtained to update the used height value (9), because Column is arranged in a single row vertically, and usedWidth does not need to be updated. But the width value needs to be updated as the expected width of the Column itself.
(10) After the traversal is completed, just like the Text in the previous section, pass the resolveSize return value into setMeasuredDimension, thus completing the Column measurement process.
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 { child -> val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, usedWidth, ) val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, usedHeight, ) (childWidthMeasureSpec, childHeightMeasureSpec)//1 usedHeight += //9 width = maxOf(width, ) } setMeasuredDimension( resolveSize(width, widthMeasureSpec), resolveSize(usedHeight, heightMeasureSpec) )//10 } private fun getChildMeasureSpec( spec: MeasureSpec, padding: Int, childDimension: Int ): MeasureSpec {//3 val childWidthSpec = MeasureSpec() val size = - padding//6 when (childDimension) {//4 LayoutParam.MATCH_PARENT -> { = = size }//5 LayoutParam.WRAP_CONTENT -> { if ( == MeasureSpec.AT_MOST || == ) { = MeasureSpec.AT_MOST = size } else if ( == ) { = = 0//7 } } else -> { = = childDimension//8 } } return childWidthSpec }//2 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { var childTop = paddingTop { child -> ( paddingLeft, childTop, paddingLeft + , childTop + ) childTop += } } }
For the onLayout method, since we already know the measured width and height of each subview, we only need to traverse each subview here and set the coordinates one by one. The coordinate setting of Column itself has been implemented in the layout method in the View.
In this way, the entire Android-like layout has been rewritten.
use
Verify our code below:
fun main() { 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 (MeasureSpec(MeasureSpec.AT_MOST, 1080), MeasureSpec(MeasureSpec.AT_MOST, 1920)) (0, 0, 1080, 1920)//3 }
(1) Define a layout page, just like the layout file written in Android, but here it is more like the writing method of the declarative UI in Flutter.
In the source code, the layout process can be simply considered to be initiated in ViewRootImpl. There is a performanceMeasure inside. PerformLayout starts the entire layout process from DecorView. The Column at (2) here is similar to the DecorView. The following two lines are similar to the layout process initiated by the method starting with perform in ViewRootImpl (because it has nothing to do with it, we do not consider the draw part).
Run the print to view the print, consistent with the expected one.
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
Summarize
- The framework code for the entire View and ViewGroup on layout (including measure, layout) is very simple, and the specific layout algorithm needs to be implemented by each subclass.
- ViewGroup's traversal of subViews, because it needs to be rewrite, all happen in the method starting with on. The determination of the measurement width and height of the parent View itself requires the measurement width and height of the child View. Therefore, the call to setMeasuredDimension is after the traversal in onMeasure; and the determination of the coordinates of the parent View does not require additional attention to the child View, so it is set in the layout method, just like the View, which occurs before the traversal of the child View by onLayout.
- The measure process is the process of passing the limit and the expected size of the View (width, height in the code) to match the limit to obtain the measured size (measuredWidth, measuredHeight).
- The fundamental purpose of the entire layout process is to determine the 4 coordinate values in the View, and this value is set in the layout method. Therefore, the call to the layout method determines the result of the layout process. The measure can be said to be an auxiliary to this process.
The above is the detailed content of implementing the Android View layout process by yourself. For more information on implementing the Android View layout process, please pay attention to my other related articles!