The last article described how an Activity builds a layout and how to measure its time. On this basis, this paper gives a plan to optimize the construction layout.
This is the fourth article in the series on Android performance optimization.
- Android performance optimization | animation to OOM? Optimized SurfaceView frame by frame resolution for frame animation
- A larger Android performance optimization | do animation to caton? Optimized SurfaceView sliding window frame reuse for frame animation
- Android performance optimization | to shorten the building layout is 20 times (on)
- Android performance optimization | to shorten the building layout is 20 times (below)
The static layout
The test layout is as follows:
The corresponding XML file is as follows (it’s a bit long, so you can skip it) :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:paddingStart="20dp"
android:paddingTop="10dp"
android:paddingEnd="20dp"
android:paddingBottom="10dp">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_back_black" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="commit"
android:textSize="30sp"
android:textStyle="bold" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_member_more" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#eeeeee" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="5dp"
android:paddingTop="30sp"
android:paddingEnd="5dp"
android:paddingBottom="30dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:background="@drawable/tag_checked_shape"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/diamond_tag" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:gravity="center"
android:padding="10dp"
android:text="gole"
android:textColor="# 389793"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="8">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="5"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="The changes were merged into release with so many bugs"
android:textSize="23sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="merge it with mercy"
android:textColor="#c4747E8B"
android:textSize="18sp" />
</LinearLayout>
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_weight="3"
android:scaleType="fitXY"
android:src="@drawable/user_portrait_gender_female" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:paddingEnd="10dp"
android:paddingBottom="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:text="2020.04.30" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#eeeeee" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginEnd="20dp"
android:background="@drawable/bg_orange_btn"
android:text="cancel" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginStart="20dp"
android:background="@drawable/bg_orange_btn"
android:text="OK" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>
Copy the code
To verify “Does a nested layout extend parsing time?” With a RelativeLayout+LinearLayout, I purposely write the deepest 5 layers of layout above.
Set it to the Activity’s ContentView, and the average build time was 24.2ms across multiple measurements. (The layout is slightly simpler, and the complexity is much lower than the interface in real projects, so there is more room for optimization in real projects)
Dynamically building a layout
If a layout in XML is called a static layout, then building a layout with Kotlin code is called a dynamic layout.
As analyzed in the previous article, static layout can’t avoid two time-consuming steps:
- Read the layout file to memory by IO operation.
- Iterate over each label in the layout file, build an instance of the control using reflection and fill in the View tree.
How much time can we save by abandoning static layouts and building them directly from Kotlin code?
So I rewrote the whole thing in pure Kotlin code, and when I was done… I almost threw up. The code is as follows:
private fun buildLayout(a): View {
return LinearLayout(this).apply { orientation = LinearLayout.VERTICAL layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) RelativeLayout(this@Factory2Activity2).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 80f.dp())
setPadding(20f.dp(), 10f.dp(), 20.0 f.dp(), 10f.dp())
ImageView(this@Factory2Activity2).apply {
layoutParams = RelativeLayout.LayoutParams(40f.dp(), 40f.dp()).apply {
addRule(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE)
addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE)
}
setImageResource(R.drawable.ic_back_black)
}.also { addView(it) }
TextView(this@Factory2Activity2).apply {
layoutParams =
RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT).apply {
addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE)
}
text = "commit"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 30f)
setTypeface(null, Typeface.BOLD)
}.also { addView(it) }
ImageView(this@Factory2Activity2).apply {
layoutParams =
RelativeLayout.LayoutParams(40f.dp(), 40f.dp()).apply {
addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE)
addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE)
}
setImageResource(R.drawable.ic_member_more)
}.also { addView(it) }
}.also { addView(it) }
View(this@Factory2Activity2).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1f.dp())
setBackgroundColor(Color.parseColor("#eeeeee"))
}.also { addView(it) }
NestedScrollView(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 500f.dp()).apply {
topMargin = 20f.dp()
}
isScrollbarFadingEnabled = true
LinearLayout(this@Factory2Activity2).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
orientation = LinearLayout.VERTICAL
setPadding(5f.dp(), 5f.dp(), 30f.dp(), 30f.dp())
LinearLayout(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
marginStart = 10f.dp()
marginEnd = 10f.dp()
}
orientation = LinearLayout.VERTICAL
setBackgroundResource(R.drawable.tag_checked_shape)
LinearLayout(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
orientation = LinearLayout.HORIZONTAL
ImageView(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(40f.dp(), 40f.dp())
setImageResource(R.drawable.diamond_tag)
}.also { addView(it) }
TextView(this@Factory2Activity2).apply {
layoutParams =
LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
marginStart = 10f.dp()
}
gravity = Gravity.CENTER
setPadding(10f.dp(), 10f.dp(), 10f.dp(), 10f.dp())
text = "gole"
setTextColor(Color.parseColor("# 389793"))
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20F)
this.setTypeface(null, Typeface.BOLD)
}.also { addView(it) }
}.also { addView(it) }
LinearLayout(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
orientation = LinearLayout.HORIZONTAL
weightSum = 8f
LinearLayout(this@Factory2Activity2).apply {
layoutParams =
LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
weight = 5f
}
orientation = LinearLayout.VERTICAL
TextView(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
text = "The changes were merged into release with so many bugs"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 23f)
}.also { addView(it) }
TextView(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
text = "merge it with mercy"
setTextColor(Color.parseColor("#c4747E8B"))
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
}.also { addView(it) }
}.also { addView(it) }
ImageView(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(100f.dp(), 100f.dp()).apply {
weight = 3f} scaleType = ImageView.ScaleType.FIT_XY setImageResource(R.drawable.user_portrait_gender_female) }.also { addView(it) } }.also { addView(it) } RelativeLayout(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
topMargin = 10f.dp()
}
setPadding(0.0.10f.dp(), 10f.dp())
TextView(this@Factory2Activity2).apply {
layoutParams =
RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)
.apply {
addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE)
}
text = "2020.04.30"
}.also { addView(it) }
}.also { addView(it) }
}.also { addView(it) }
}.also { addView(it) }
}.also { addView(it) }
View(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1f.dp())
setBackgroundColor(Color.parseColor("#eeeeee"))
}.also { addView(it) }
RelativeLayout(this@Factory2Activity2).apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
topMargin = 40f.dp()
}
LinearLayout(this@Factory2Activity2).apply {
layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE)
}
orientation = LinearLayout.HORIZONTAL
Button(this@Factory2Activity2).apply {
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
rightMargin = 20f.dp()
gravity = Gravity.LEFT
}
setBackgroundResource(R.drawable.bg_orange_btn)
text = "cancel"
}.also {
addView(it)
}
Button(this@Factory2Activity2).apply {
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
leftMargin = 20f.dp()
gravity = Gravity.RIGHT
}
setBackgroundResource(R.drawable.bg_orange_btn)
text = "OK"
}.also { addView(it) }
}.also { addView(it) }
}.also { addView(it) }
}
}
Copy the code
Describing the above code in pseudocode, the structure looks like this:
Container controls. apply {child controls. apply {// set control properties}.also {addView(it)}}Copy the code
The code is stinky, long, redundant, and completely unreadable. If you want to fine-tune the control that shows the gem, you can try it out. I can’t find the control anyway.
But after running through the test code, I was pleasantly surprised to find that the average time to build a layout was only 1.32ms, 1/20 of the time of a static layout.
At first I thought the nested layout would be time-consuming, so I flattened the nested layout with ConstraintLayout.
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/ivBack"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:src="@drawable/ic_back_black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvCommit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="commit"
android:textSize="30sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/ivBack"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/ivBack" />
<ImageView
android:id="@+id/ivMore"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="20dp"
android:src="@drawable/ic_member_more"
app:layout_constraintBottom_toBottomOf="@id/ivBack"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/ivBack" />
<View
android:id="@+id/vDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="10dp"
android:background="#eeeeee"
app:layout_constraintTop_toBottomOf="@id/ivBack" />
<View
android:id="@+id/bg"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/tag_checked_shape"
app:layout_constraintBottom_toBottomOf="@id/tvTime"
app:layout_constraintEnd_toEndOf="@id/ivDD"
app:layout_constraintStart_toStartOf="@id/ivD"
app:layout_constraintTop_toTopOf="@id/ivD" />
<ImageView
android:id="@+id/ivD"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="20dp"
android:layout_marginTop="40dp"
android:src="@drawable/diamond_tag"
app:layout_constraintStart_toStartOf="@id/ivBack"
app:layout_constraintTop_toBottomOf="@id/vDivider" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:padding="10dp"
android:text="gole"
android:textColor="# 389793"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/ivD"
app:layout_constraintStart_toEndOf="@id/ivD"
app:layout_constraintTop_toTopOf="@id/ivD" />
<TextView
android:id="@+id/tvC"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="The changes were merged into release with so many bugs"
android:textSize="23sp"
app:layout_constraintEnd_toStartOf="@id/ivDD"
app:layout_constraintStart_toStartOf="@id/ivD"
app:layout_constraintTop_toBottomOf="@id/ivD" />
<ImageView
android:id="@+id/ivDD"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginEnd="20dp"
android:src="@drawable/user_portrait_gender_female"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tvC"
app:layout_constraintTop_toTopOf="@id/tvC" />
<TextView
android:id="@+id/tvSub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="merge it with mercy"
android:textColor="#c4747E8B"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="@id/ivD"
app:layout_constraintTop_toBottomOf="@id/tvC" />
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="2020.04.30"
app:layout_constraintEnd_toEndOf="@id/ivDD"
app:layout_constraintTop_toBottomOf="@id/ivDD" />
<TextView
android:id="@+id/tvCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
android:background="@drawable/bg_orange_btn"
android:paddingStart="30dp"
android:paddingTop="10dp"
android:paddingEnd="30dp"
android:paddingBottom="10dp"
android:text="cancel"
android:layout_marginBottom="20dp"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tvOK"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tvOK"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_orange_btn"
android:paddingStart="30dp"
android:paddingTop="10dp"
android:layout_marginBottom="20dp"
android:paddingEnd="30dp"
android:paddingBottom="10dp"
android:text="OK"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/tvCancel" />
<View
app:layout_constraintBottom_toTopOf="@id/tvCancel"
android:layout_marginBottom="20dp"
android:background="#eeeeee"
android:layout_width="match_parent"
android:layout_height="1dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code
Zero nesting is achieved this time, and the code is rerunned with expectations. But the time it takes to parse the layout hasn’t changed at all… All right.
Since there is such a big performance gap between static and dynamic layouts, improve the readability of dynamic layout code!!
DSL
DSLS are a great way to improve the readability of your build code!
A domain Specific Language (DSL) corresponds to a concept called a general purpose programming language, which has a complete set of capabilities to solve almost any problem that can be solved by a computer. Java is one of these types. Domain-specific languages focus only on specific tasks, such as SQL focusing only on manipulating databases, HTML focusing only on expressing hypertext.
Why do you need domain-specific languages when general-purpose programming languages can solve all problems? Because it can express domain-specific operations in a more compact syntax than the equivalent code in a general-purpose programming language. For example, when executing an SQL statement, you don’t need to start by declaring a class and its methods.
Tighter syntax means cleaner apis. Each class in an application provides the possibility for other classes to interact with it, and ensuring that these interactions are easy to understand and can be succinct is critical to software maintainability.
There is one characteristic of DSLS that ordinary apis do not have: DSLS have structure. Lambdas with receivers make it easy to build structured apis.
Lambda with a receiver
It’s a special kind of lambda, which is unique to Kotlin. Think of it as “an anonymous extension function declared for the recipient.” (An extension function is a feature that adds functionality to a class outside of the class.)
The function body of a lambda with a receiver has access to all non-private members of the receiver in addition to the members of its class, a feature that makes it easy to build structures.
When lambdas with receivers are paired with higher-order functions, building a structured API becomes a breeze.
Higher-order functions
It’s a special function whose argument or return value is another function.
For example, the set extension function filter() is a higher-order function:
// The argument to filter is a lambda with a receive
public inline fun <T>可迭代<T>.filter(predicate: (T) - >Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
Copy the code
You can use it to filter elements in a collection:
students.filter { age > 18 }
Copy the code
This is a call to a structured API (not visible in Java), although this structure benefits from a kotlin convention (if the function has only one argument and it is a lambda, the parentheses of the function argument list can be omitted). But more critical is the inner workings of the lambda. Thanks to lambdas with recipients, Age > 18 runs in a different context than its caller. In this context, it is easy to access Student’s member student. age (you can omit this when referring to age).
Let’s use this technique to improve the readability of the dynamically built layout code.
Dynamic Layout DSL
Reconstructing the above layout with a DSL would look like this:
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
ImageView {
layout_id = "ivBack"
layout_width = 40
layout_height = 40
margin_start = 20
margin_top = 20
src = R.drawable.ic_back_black
start_toStartOf = parent_id
top_toTopOf = parent_id
onClick = { onBackClick() }
}
TextView {
layout_width = wrap_content
layout_height = wrap_content
text = "commit"
textSize = 30f
textStyle = bold
align_vertical_to = "ivBack"
center_horizontal = true
}
ImageView {
layout_width = 40
layout_height = 40
src = R.drawable.ic_member_more
align_vertical_to = "ivBack"
end_toEndOf = parent_id
margin_end = 20
}
View {
layout_id = "vDivider"
layout_width = match_parent
layout_height = 1
margin_top = 10
background_color = "#eeeeee"
top_toBottomOf = "ivBack"
}
Layer {
layout_id = "layer"
layout_width = wrap_content
layout_height = wrap_content
referenceIds = "ivDiamond,tvTitle,tvContent,ivAvatar,tvTime,tvSub"
background_res = R.drawable.tag_checked_shape
start_toStartOf = "ivDiamond"
top_toTopOf = "ivDiamond"
bottom_toBottomOf = "tvTime"
end_toEndOf = "tvTime"
}
ImageView {
layout_id = "ivDiamond"
layout_width = 40
layout_height = 40
margin_start = 20
margin_top = 40
src = R.drawable.diamond_tag
start_toStartOf = "ivBack"
top_toBottomOf = "vDivider"
}
TextView {
layout_id = "tvTitle"
layout_width = wrap_content
layout_height = wrap_content
margin_start = 5
gravity = gravity_center
text = "gole"
padding = 10
textColor = "# 389793"
textSize = 20f
textStyle = bold
align_vertical_to = "ivDiamond"
start_toEndOf = "ivDiamond"
}
TextView {
layout_id = "tvContent"
layout_width = 0
layout_height = wrap_content
margin_top = 5
text = "The changes were merged into release with so many bugs"
textSize = 23f
start_toStartOf = "ivDiamond"
top_toBottomOf = "ivDiamond"
end_toStartOf = "ivAvatar"
}
ImageView {
layout_id = "ivAvatar"
layout_width = 100
layout_height = 100
margin_end = 20
src = R.drawable.user_portrait_gender_female
end_toEndOf = parent_id
start_toEndOf = "tvContent"
top_toTopOf = "tvContent"
}
TextView {
layout_id = "tvSub"
layout_width = wrap_content
layout_height = wrap_content
text = "merge it with mercy"
textColor = "#c4747E8B"
textSize = 18f
start_toStartOf = "ivDiamond"
top_toBottomOf = "tvContent"
}
TextView {
layout_id = "tvTime"
layout_width = wrap_content
layout_height = wrap_content
margin_top = 20
text = "2020.04.30"
end_toEndOf = "ivAvatar"
top_toBottomOf = "ivAvatar"
}
TextView {
layout_id = "tvCancel"
layout_width = wrap_content
layout_height = wrap_content
margin_end = 30
background_res = R.drawable.bg_orange_btn
padding_start = 30
padding_top = 10
padding_end = 30
padding_bottom = 10
text = "cancel"
margin_bottom = 20
textSize = 20f
textStyle = bold
bottom_toBottomOf = parent_id
end_toStartOf = "tvOk"
start_toStartOf = parent_id
horizontal_chain_style = packed
}
TextView {
layout_id = "tvOk"
layout_width = wrap_content
layout_height = wrap_content
background_res = R.drawable.bg_orange_btn
padding_start = 30
padding_top = 10
margin_bottom = 20
padding_end = 30
padding_bottom = 10
text = "Ok"
textSize = 20f
textStyle = bold
bottom_toBottomOf = parent_id
end_toEndOf = parent_id
horizontal_chain_style = packed
start_toEndOf = "tvCancel"}}}Copy the code
After refactoring, the dynamic layout code is as readable as the static layout, and even cleaner than the static layout.
Building control
The class name of each control in the code is an extension method, and the way to build a container control is as follows:
inline fun Context.ConstraintLayout(init: ConstraintLayout. () - >Unit): ConstraintLayout =
ConstraintLayout(this).apply(init)
Copy the code
The container controls are constructed using the Context extension method, and the layout is built wherever the Context is present.
The extension method calls the constructor directly and applies the lambda for which the property was initialized. The lambda is a LabMDA with a receiver, whose receiver is ConstraintLayout, a feature unique to Kotlin that allows an additional non-private member of an object to be accessed in the body of a lambda function. In this case, the lambda expression init has access to all non-private members of ConstraintLayout in the function body, making it easy to set control properties in the function body.
With this extension function, you can build a container control like this (you can ignore the attribute assignment logic until the next section) :
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
}
Copy the code
The above paragraph is equivalent to the following XML:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
Copy the code
Compared to XML, it is more concise by omitting some repetitive information.
Child controls are built using the ViewGroup extension method:
inline fun ViewGroup.TextView(init: TextView. () - >Unit) =
TextView(context).apply(init).also { addView(it) }
Copy the code
When the child controls are built and need to be filled with container controls, the extension methods defined as ViewGroup can easily call addView().
Controls are built inline with the keyword inline. The compiler tils code with an inline function body to the call, avoiding a single function call and the time and space overhead of a function call (creating a stack frame on the stack). By default, every lambda in Kotlin is compiled into an anonymous class unless the lambda is inlined. The inlined build method allows no function calls to occur while the layout is being built, and no anonymous inner classes are created.
You can now add child controls to the container control like this:
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
TextView {
layout_width = wrap_content
layout_height = wrap_content
}
}
Copy the code
The downside of this definition is that textViews can only be built within viewgroups. If you need to build textViews separately, you can mimic the container controls.
inline fun Context.TextView(init: TextView. () - >Unit) =
TextView(this).apply(init)
Copy the code
Setting control Properties
Each attribute in XML has a Corresponding Java method, and calling the method directly makes dynamic build code unreadable.
Is there any way to turn a method call into a property assignment statement? — Extended attributes:
inline var View.background_color: String
get() {
return ""
}
set(value) {
setBackgroundColor(Color.parseColor(value))
}
Copy the code
Added an extended property named background_color to the View, which is a String variable for which we need to define a value and set method. When this property is assigned, the set() method is called, in which view.setBackgroundColor () is called to set the background color.
You can now set the control background color like this:
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
background_color = "#ffff00"
}
Copy the code
In particular, for the following “or” properties:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal|top"/>
Copy the code
Instead of + :
TextView {
layout_width = wrap_content
layout_height = wrap_content
gravity = gravity_center_horizontal + gravity_top
}
Copy the code
Modify the layout properties incrementally
In the example above, the background color is a separate property, that is, changing it does not affect the other properties. But modifying layout properties is done in batches. When you want to change only one of the values, you must change incrementally:
inline var View.padding_top: Int
get() {
return 0
}
set(value) {
setPadding(paddingLeft, value.dp(), paddingRight, paddingBottom)
}
Copy the code
Padding_top is defined as an extended property of the View, so you can easily access the original paddingLeft, paddingRight, and paddingBottom of the View in the set() method, so that these three properties are left as is, and only the paddingTop is modified.
Dp () is an extension method that converts an Int value to a DP value based on the current screen density:
fun Int.dp(a): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
Resources.getSystem().displayMetrics
).toInt()
Copy the code
Setting the width and height of the control also requires incremental changes:
inline var View.layout_width: Int
get() {
return 0
}
set(value) {
val w = if (value > 0) value.dp() else value
valh = layoutParams? .height ? :0
layoutParams = ViewGroup.MarginLayoutParams(w, h)
}
Copy the code
When set wide, read the original high and new ViewGroup. MarginLayoutParams, again for the layoutParams assignment. In order to select the generality, ViewGroup MarginLayoutParams, it is the father of all the other LayoutParams class.
A more complex example is the relative layout property in ContraintLayout:
inline var View.start_toStartOf: String
get() {
return ""
}
set(value) {
layoutParams = layoutParams.append {
//'toLayoutId() is the method to generate the control ID, described in the next section. '
startToStart = value.toLayoutId()
startToEnd = -1}}Copy the code
Every relative layout in the XML attribute corresponds to ContraintLayout. LayoutParams instance of an Int value (ID is type Int) control. So you must take the original LayoutParams instance and assign a value to the corresponding new attribute, like this:
inline var View.start_toStartOf: String
get() {
return ""
}
set(value) {layoutParams = layoutParams. Apply {startToStart = Control ID//'-1 means no relative constraint '
startToEnd = -1}}Copy the code
But set high, wide construction is ViewGroup MarginLayoutParams instance, it is not relative to the layout of the property. So I need the original ViewGroup. MarginLayoutParams the high and wide margin value to copy out, to build a ContraintLayout. LayoutParams:
fun ViewGroup.LayoutParams.append(set: ConstraintLayout.LayoutParams. () - >Unit) =
//' if the layout is restricted, increment assignment directly '
(this as? ConstraintLayout.LayoutParams)? .apply(set) ?:
//' Otherwise copy the value of the margin layout parameter to the limit layout parameter and incrementing the value. '
(this as? ViewGroup.MarginLayoutParams)? .toConstraintLayoutParam()? .apply(set)
//' convert margin layout parameters to restricted layout parameters'
fun ViewGroup.MarginLayoutParams.toConstraintLayoutParam(a) =
ConstraintLayout.LayoutParams(width, height).also { it ->
it.topMargin = this.topMargin
it.bottomMargin = this.bottomMargin
it.marginStart = this.marginStart
it.marginEnd = this.marginEnd
}
Copy the code
One drawback to this approach is that you must first set the width and height of the control before setting the relative layout properties.
Generating a Control ID
View. SetId (int ID) receives an int value, but an int value has no semantics and does not function as a tag control, so the extended attribute layout_id is a String:
inline var View.layout_id: String
get() {
return ""
}
set(value) {
id = value.toLayoutId()
}
//' convert String to Int '
fun String.toLayoutId(a):Int{
var id = java.lang.String(this).bytes.sum()
if (id == 48) id = 0
return id
}
Copy the code
In order to call view.setid (), the String must be converted to an Int. The method is to convert the String to an array of bytes, and then to accumulate the array. But the String in Kotlin does not have getBytes(), so you can only construct java.lang.string explicitly.
The reason to hard-code 48 is because:
public class ConstraintLayout extends ViewGroup {
public static class LayoutParams extends MarginLayoutParams {
public static final int PARENT_ID = 0; }}Copy the code
I redefine the constant as a String:
val parent_id = "0"
Copy the code
According to the toLayoutId() algorithm, the corresponding value of “0” is 48.
A better approach is to find the inverse of toLayoutId(), which is to say, what should the input be when the output of this function is zero? Unfortunately, I couldn’t figure out how to do it. Hope to know the small partner to dial ~
You can now set the control ID like this:
ConstraintLayout {
layout_id = "cl"
layout_width = match_parent
layout_height = match_parent
background_color = "#ffff00"
ImageView {
layout_id = "ivBack"
layout_width = 40
layout_height = 40
src = R.drawable.ic_back_black
start_toStartOf = parent_id
top_toTopOf = parent_id
}
}
Copy the code
Renames control properties
In order to keep the construction syntax as simple as possible, constants with class names have been redefined, such as:
val match_parent = ViewGroup.LayoutParams.MATCH_PARENT
val wrap_content = ViewGroup.LayoutParams.WRAP_CONTENT
val constraint_start = ConstraintProperties.START
val constraint_end = ConstraintProperties.END
val constraint_top = ConstraintProperties.TOP
val constraint_bottom = ConstraintProperties.BOTTOM
val constraint_baseline = ConstraintProperties.BASELINE
val constraint_parent = ConstraintProperties.PARENT_ID
Copy the code
New properties: Composite properties
With extended attributes, you can optionally dynamically add attributes that are not in the original XML.
ConstraintLayout to align a control vertically in ConstraintLayout, you need to set the values of the two properties top_toTopOf and bottom_toBottomOf to the target control ID. You can simplify this step by using the extend properties:
inline var View.align_vertical_to: String
get() {
return ""
}
set(value) {
top_toTopOf = value
bottom_toBottomOf = value
}
Copy the code
Top_toTopOf and bottom_toBottomOf are similar to start_toStartOf.
Similarly, you can define ALIGN_horizontal_to.
New property: View click listener
The following code sets the click event with an extended property:
var View.onClick: (View) -> Unit
get() {
return{}}set(value) {
setOnClickListener { v -> value(v) }
}
Copy the code
Extend the onClick property for the View, which is of function type. Then you can set the click event like this:
private fun buildViewByClDsl(a): View =
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
ImageView {
layout_id = "ivBack"
layout_width = 40
layout_height = 40
margin_start = 20
margin_top = 20
src = R.drawable.ic_back_black
start_toStartOf = parent_id
top_toTopOf = parent_id
onClick = onBackClick
}
}
valonBackClick = { v : View -> activity? .finish() }Copy the code
Thanks to function types, you can wrap the click logic in a lambda and assign it to the variable onBackClick.
Added property: Column table entry click event
RecyclerView does not have a child click event listener, which can also be solved by extending properties:
//' extend the RecyclerView entry by clicking on the listener property '
var RecyclerView.onItemClick: (View, Int) - >Unit
get() {
return{_, _ ->}}set(value) {
setOnItemClickListener(value)
}
//' Click on the listener for RecyclerView extension entries'
fun RecyclerView.setOnItemClickListener(listener: (View.Int) - >Unit) {
//' Set touch listener for RecyclerView child control '
addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
//' Construct a gesture detector to parse click events'
val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener {
override fun onShowPress(e: MotionEvent?).{}override fun onSingleTapUp(e: MotionEvent?).: Boolean {
//' When a click event occurs, find the child control under the click coordinates and call back the listener 'e? .let { findChildViewUnder(it.x, it.y)? .let { child -> listener(child, getChildAdapterPosition(child)) } }return false
}
override fun onDown(e: MotionEvent?).: Boolean {
return false
}
override fun onFling(e1: MotionEvent? , e2:MotionEvent? , velocityX:Float, velocityY: Float): Boolean {
return false
}
override fun onScroll(e1: MotionEvent? , e2:MotionEvent? , distanceX:Float, distanceY: Float): Boolean {
return false
}
override fun onLongPress(e: MotionEvent?).{}})override fun onTouchEvent(rv: RecyclerView, e: MotionEvent){}//' Resolve touch events while intercepting touch events'
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
gestureDetector.onTouchEvent(e)
return false
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean){}})}Copy the code
You can then set the entry click event for RecyclerView like this:
RecyclerView {
layout_id = "rvTest"
layout_width = match_parent
layout_height = 300
onItemClick = onListItemClick
}
val onListItemClick = { v: View, i: Int ->
Toast.makeText(context, "item $i is clicked", Toast.LENGTH_SHORT).show()
}
Copy the code
New property: Text change listener
Both of these new properties can be represented as a function type variable. If you have multiple callbacks, such as listening for text changes in EditText, you can write:
inline var TextView.onTextChange: TextWatcher
get() {
return TextWatcher()
}
set(value) {
// Set a text change listener for the control
val textWatcher = object : android.text.TextWatcher {
override fun afterTextChanged(s: Editable?). {
/ / will be the realization of the callback entrusted to TextWatcher afterTextChanged
value.afterTextChanged.invoke(s)
}
override fun beforeTextChanged(text: CharSequence? ,start:Int,count: Int,after:Int) {
/ / will be the realization of the callback entrusted to TextWatcher beforeTextChanged
value.beforeTextChanged.invoke(text, start, count, after)
}
override fun onTextChanged(text: CharSequence? , start:Int, before: Int, count: Int) {
/ / will be the realization of the callback entrusted to TextWatcher onTextChanged
value.onTextChanged.invoke(text, start, before, count)
}
}
addTextChangedListener(textWatcher)
}
Copy the code
Set the listener for the control and delegate the implementation of the callback to the lambda in TextWatcher:
// Class TextWatcher contains variables of three function types that correspond to three callbacks in the Android.text. TextWatcher interface
class TextWatcher(
varbeforeTextChanged: ( text: CharSequence? , start:Int,
count: Int,
after: Int) - >Unit= {_, _, _, _ ->}varonTextChanged: ( text: CharSequence? , start:Int,
count: Int,
after: Int) - >Unit= {_, _, _, _ ->}var afterTextChanged: (text: Editable?) -> Unit= {})Copy the code
Then you can use it like this:
EditText {
layout_width = match_parent
layout_height = 50
textSize = 20f
background_color = "#00ffff"
top_toBottomOf = "rvTest"onTextChange = textWatcher { onTextChanged = { text: CharSequence? , start:Int, count: Int, after: Int ->
Log.v("test"."onTextChanged, text=${text}")}}}Copy the code
TextWatcher is a top-level function that builds textWatcher instances:
fun textWatcher(init: TextWatcher. () - >Unit): TextWatcher = TextWatcher().apply(init)
Copy the code
findViewById
How do I get a reference to a control instance? Thanks to the syntactic sugar of DSLS, there is a new way to build dynamic layouts:
class MainActivity : AppCompatActivity() {
private var ivBack:ImageView? = null
private var tvTitle:TextView? = null
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
ivBack = ImageView {
layout_id = "ivBack"
layout_width = 40
layout_height = 40
margin_start = 20
margin_top = 20src = R.drawable.ic_back_black start_toStartOf = parent_id top_toTopOf = parent_id } tvTitle = TextView { layout_width = wrap_content layout_height = wrap_content text ="commit"
textSize = 30f
textStyle = bold
align_vertical_to = "ivBack"
center_horizontal = true}}}override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(rootView)
}
}
Copy the code
In addition to this way, there is a common way:
fun <T : View> View.find(id: String): T = findViewById<T>(id.toLayoutId())
fun <T : View> AppCompatActivity.find(id: String): T = findViewById<T>(id.toLayoutId())
Copy the code
The cool part of DSL layout
Simplify control logic for multi-state interfaces
Real projects often have a scenario where “different types of controls are displayed in different states at certain locations on the interface”. The common practice is to declare different states of the control in the layout file, and then through the code according to the status of setVisibility(view.visible) + setVisibility(view.gone) control.
Because the DSL is Kotlin code, conditional logic can be inserted without barrier:
class AFragment : Fragment() {
// Interface status
private val type bylazy { arguments? .getInt("layout-type")}private val rootView: ConstraintLayout? by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
// Add different views according to the state of the interface
if (type == 1) {
TextView {
layout_width = wrap_content
layout_height = wrap_content
textSize = 14f
bottom_toBottomOf = parent_id
center_horizontal = true
onClick = { _ -> startActivityA() }
}
} else {
ImageView {
layout_width = match_parent
layout_height = 40
bottom_toBottomOf = parent_id
onClick = { _ -> startActivityB() }
}
}
}
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return rooView
}
}
Copy the code
Dynamically building a layout
The content of the interface is returned by the server, that is, the number of controls can not be determined in advance. In addition to usingRecyclerView
Alternatively, DSLS can be used to dynamically build layouts based on data:
class GameDialogFragment : DialogFragment() {
// Create a vertical root layout
private val rootView: LinearLayout? by lazy {
LinearLayout {
layout_width = match_parent
layout_height = 0
height_percentage = 0.22 f
orientation = vertical
top_toTopOf = parent_id
}
}
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return rooView
}
fun onGameReturn(gameBeans: GameBean){
buildGameLayout(gameBeans)
}
private fun buildGameLayout(gameBeans: GameBean) {
// Iterate over the data and add controls to the root layout
rootView.apply {
// Title of game properties
gameBeans.forEach { game ->
TextView {
layout_width = wrap_content
layout_height = wrap_content
textSize = 14f
text = game.attrName
}
// Wrap a container control
LineFeedLayout {
layout_width = match_parent
layout_height = wrap_content
horizontal_gap = 8
vertical_gap = 8
// Attribute name of the game
game.attrs.forEachIndexed { index, attr ->
TextView {
layout_width = wrap_content
layout_height = wrap_content
textSize = 12f
text = attr.name
bacground_res = if (attr.isDefault) R.drawable.select else R.drawable.unselect
}
}
}
}
}
}
}
Copy the code
talk is cheap, show me the code
The GitHub code writes all of the above extension methods and properties in a layout. kt file. After the business interface introduces all of the contents in this file, it can be completed when writing dynamic layouts (only lists the extensions of common controls and their properties, you can add them if needed).
The code is linked here