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.

  1. Android performance optimization | animation to OOM? Optimized SurfaceView frame by frame resolution for frame animation
  2. A larger Android performance optimization | do animation to caton? Optimized SurfaceView sliding window frame reuse for frame animation
  3. Android performance optimization | to shorten the building layout is 20 times (on)
  4. 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:

  1. Read the layout file to memory by IO operation.
  2. 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 usingRecyclerViewAlternatively, 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