上一篇講述了 Activity 構建佈局的過程,及測量其耗時的方法。這一篇在此基礎上給出優化構建佈局的方案。java
這是 Android 性能優化系列文章的第四篇,文章列表以下:android
測試佈局以下圖所示:git
與之對應的 xml 文件以下(有點長,能夠直接跳過):github
<?xml version="1.0" encoding="utf-8"?>
<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> 複製代碼
爲了驗證「嵌套佈局是否會延長解析時間?」,特地用RelativeLayout
+LinearLayout
寫了上面最深 5 層嵌套的佈局。web
把它設置爲 Activity 的 ContentView,經屢次測量構建平均耗時爲 24.2 ms 。(佈局略簡單,複雜度遠低於真實項目中的界面,遂真實項目中的優化空間更大)算法
若是把 xml 中的佈局稱爲靜態佈局的話,那用 Kotlin 代碼構建佈局就能夠稱爲動態佈局。數據庫
正如上一篇分析的那樣,靜態佈局避免不了兩個耗時的步驟:編程
那棄用靜態佈局,直接使用 Kotlin 代碼構建佈局,能節約多少時間?數組
因而我用純 Kotlin 代碼重寫了一遍及局,寫完。。。差點吐了,代碼以下:性能優化
private fun buildLayout(): 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.0f.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) } } } 複製代碼
用僞代碼描述上述代碼,結構就是這樣的:
容器控件.apply {
子控件.apply {
//設置控件屬性
}.also { addView(it) }
}
複製代碼
代碼又臭又長又冗餘,徹底沒有可讀性。若要微調其中顯示寶石的控件,你能夠試下,反正我是找不到那個控件了。
但跑了一下測試代碼,驚喜地發現構建佈局的平均耗時只有 1.32 ms,時間是靜態佈局的 1/20 。
一開始我覺得是嵌套佈局致使特別耗時,因而用ConstraintLayout
將嵌套扁平化,代碼以下:
<?xml version="1.0" encoding="utf-8"?>
<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> 複製代碼
此次作到了零嵌套,帶着指望從新運行了一遍代碼。但解析佈局耗時絲毫沒有變化。。。好吧
既然靜態佈局和動態佈局有這麼大的性能差距,那就改善一下動態佈局代碼的可讀性!!
DSL 在以前的文章中有屢次亮相,好比 這篇 引出了 DSL 的概念,引用以下:
DSL = domain specific language,即「特定領域語言」,與它對應的一個概念叫「通用編程語言」,通用編程語言有一系列完善的能力來解決幾乎全部能被計算機解決的問題,像 Java 就屬於這種類型。而特定領域語言只專一於特定的任務,好比 SQL 只專一於操縱數據庫,HTML 只專一於表述超文本。
再好比這篇 是 DSL 在項目中的實戰,介紹瞭如何用 DSL 從新定義構建動畫的代碼。
一樣的思路也能夠運用到構建佈局上,用 DSL 從新構建上面的佈局以下:
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" } } } 複製代碼
重構以後的動態佈局代碼,有了和靜態佈局同樣的可讀性,甚至比靜態佈局更簡潔了。
代碼中每個控件的類名都是一個擴展方法,構建容器控件的方法以下:
inline fun Context.ConstraintLayout(init: ConstraintLayout.() -> Unit): ConstraintLayout =
ConstraintLayout(this).apply(init) 複製代碼
容器控件的構造都經過Context
的擴展方法實現,只要有Context
的地方就能構建佈局。
擴展方法會直接調用構造函數並應用爲其初始化屬性的 lambda。該 lambda 是一個帶接收者的labmda
,它的接收者是ConstraintLayout
,Kotlin 獨有的這個特性使得 lambda 函數體中能夠額外地多訪問一個對象的非私有成員。本例中 lambda 表達式init
的函數體中能夠訪問ConstraintLayout
的全部非私有成員,這樣就能輕鬆地在函數體中設置控件屬性。
有了這個擴展函數,就能夠這樣構建容器控件(可先忽略屬性賦值邏輯,下一節再介紹):
ConstraintLayout { layout_width = match_parent layout_height = match_parent } 複製代碼
上述這段等價於下面的 xml:
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> 複製代碼
相較於 xml,省略了一些重複信息,顯得更簡潔。
構建子控件經過ViewGroup
的擴展方法實現:
inline fun ViewGroup.TextView(init: TextView.() -> Unit) =
TextView(context).apply(init).also { addView(it) } 複製代碼
子控件構建完畢後須要填入容器控件,定義成ViewGroup
的擴展方法就能方便的調用addView()
。
控件的構建方法都經過關鍵詞inline
進行了內聯,編譯器會將帶有inline
函數體中的代碼平鋪到調用處,這樣就避免了一次函數調用,函數調用也有時間和空間上的開銷(在棧中建立棧幀)。默認狀況下、每一個 Kotlin 中的 lambda 都會被編譯成一個匿名類,除非 lambda 被內聯。被內聯的構建方法使得構建佈局時不會發生函數調用,而且也不會建立匿名內部類。
如今就能夠像這樣爲容器控件添加子控件了:
ConstraintLayout { layout_width = match_parent layout_height = match_parent TextView { layout_width = wrap_content layout_height = wrap_content } } 複製代碼
這樣定義的缺點是:只能在ViewGroup
中構建TextView
,如有單獨構建的需求,能夠模仿容器控件的構建方法:
inline fun Context.TextView(init: TextView.() -> Unit) =
TextView(this).apply(init) 複製代碼
xml 中每個屬性都有對應的 Java 方法,直接調用方法使得動態構建代碼可讀性不好。
有什麼辦法能夠把方法調用轉化成屬性賦值語句?—— 擴展屬性:
inline var View.background_color: String
get() { return "" } set(value) { setBackgroundColor(Color.parseColor(value)) } 複製代碼
爲View
增長了名爲background_color
的擴展屬性,它是String
類型的變量,需爲其定義取值和設置方法。當該屬性被賦值時,set()
方法會被調用,在其中調用了View.setBackgroundColor()
來設置背景色。
如今就能夠像這樣設置控件背景色了:
ConstraintLayout {
layout_width = match_parent layout_height = match_parent background_color = "#ffff00" } 複製代碼
特別地,對於下面這種「可或」的屬性:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal|top"/> 複製代碼
改成+
:
TextView { layout_width = wrap_content layout_height = wrap_content gravity = gravity_center_horizontal + gravity_top } 複製代碼
上面的例子中,背景色是一個獨立的屬性,即修改它不會影響到其餘屬性。但修改佈局屬性都是批量的。當只想修改其中一個屬性值時,就必須增量修改:
inline var View.padding_top: Int
get() { return 0 } set(value) { setPadding(paddingLeft, value.dp(), paddingRight, paddingBottom) } 複製代碼
padding_top
被定義爲View
的擴展屬性,因此在set()
方法中能輕鬆訪問到View
原有的paddingLeft
,paddingRight
,paddingBottom
,以便使這三個屬性保持原樣,而只修改paddingTop
。
dp()
是一個擴展方法,用來將 Int 值根據當前屏幕密度轉換成 dp 值:
fun Int.dp(): Int =
TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics ).toInt() 複製代碼
爲控件設置寬高也須要增量修改:
inline var View.layout_width: Int
get() { return 0 } set(value) { val w = if (value > 0) value.dp() else value val h = layoutParams?.height ?: 0 layoutParams = ViewGroup.MarginLayoutParams(w, h) } 複製代碼
在設置寬時,讀取原有高,並新建ViewGroup.MarginLayoutParams
,從新爲layoutParams
賦值。爲了通用性,選擇了ViewGroup.MarginLayoutParams
,它是全部其餘LayoutParams
的父類。
一個更復雜的例子是ContraintLayout
中的相對佈局屬性:
inline var View.start_toStartOf: String
get() { return "" } set(value) { layoutParams = layoutParams.append { //'toLayoutId()是生成控件id的方法,下一節會介紹' startToStart = value.toLayoutId() startToEnd = -1 } } 複製代碼
在 xml 中每個相對佈局屬性都對應於ContraintLayout.LayoutParams
實例中的一個 Int 值(控件 ID 是 Int 類型)。因此必須獲取原LayoutParams
實例併爲對應的新增屬性賦值,就像這樣:
inline var View.start_toStartOf: String
get() { return "" } set(value) { layoutParams = layoutParams.apply { startToStart = 控件ID //'-1表示沒有相對約束' startToEnd = -1 } } 複製代碼
但設置寬高時,構造的是ViewGroup.MarginLayoutParams
實例,它並無相對佈局的屬性。因此須要將原ViewGroup.MarginLayoutParams
中的寬高和邊距值複製出來,從新構建一個ContraintLayout.LayoutParams
:
fun ViewGroup.LayoutParams.append(set: ConstraintLayout.LayoutParams.() -> Unit) =
//'若是是限制佈局則直接增量賦值' (this as? ConstraintLayout.LayoutParams)?.apply(set) ?: //'不然將邊距佈局參數值拷貝到限制佈局參數中,再增量賦值' (this as? ViewGroup.MarginLayoutParams)?.toConstraintLayoutParam()?.apply(set) //'將邊距佈局參數轉換成限制佈局參數' fun ViewGroup.MarginLayoutParams.toConstraintLayoutParam() = ConstraintLayout.LayoutParams(width, height).also { it -> it.topMargin = this.topMargin it.bottomMargin = this.bottomMargin it.marginStart = this.marginStart it.marginEnd = this.marginEnd } 複製代碼
這個方案有一個缺點:必須先爲控件設置寬高,再設置相對佈局屬性。
View.setId(int id)
接收 int 類型的值,但 int 值沒有語義,起不到標記控件的做用,因此擴展屬性layout_id
是 String 類型的:
inline var View.layout_id: String
get() { return "" } set(value) { id = value.toLayoutId() } //'將String轉化成對應的Int值' fun String.toLayoutId():Int{ var id = java.lang.String(this).bytes.sum() if (id == 48) id = 0 return id } 複製代碼
String 必須轉化成 Int 才能調用View.setId()
,採用的方法是:先將 String 轉化成 byte 數組,而後對數組累加。但 Kotlin 中的 String 沒有getBytes()
,因此只能顯示地構造java.lang.String
。
之因此要硬編碼48
是由於:
public class ConstraintLayout extends ViewGroup {
public static class LayoutParams extends MarginLayoutParams { public static final int PARENT_ID = 0; } } 複製代碼
而我把該常量從新定義成 String 類型:
val parent_id = "0"
複製代碼
經過toLayoutId()
算法,"0"
對應值爲 48。
更好的辦法是找出toLayoutId()
算法的逆算法,即當該函數輸出爲 0 時,輸入應該是多少?惋惜並想不出如何實現。望知道的小夥伴點撥~
如今就能夠像這樣設置控件 ID 了:
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 } } 複製代碼
爲了讓構建語法儘量的精簡,原先帶有類名的常量都被從新定義了,好比:
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 複製代碼
利用擴展屬性,還能夠任意動態新增一些原先 xml 中沒有的屬性。
在ConstraintLayout
中若是想縱向對齊一個控件,須要將兩個屬性的值設置爲目標控件ID,分別是top_toTopOf
和bottom_toBottomOf
,若經過擴展屬性就能簡化這個步驟:
inline var View.align_vertical_to: String
get() { return "" } set(value) { top_toTopOf = value bottom_toBottomOf = value } 複製代碼
其中的top_toTopOf
和bottom_toBottomOf
和上面列舉的start_toStartOf
相似,再也不贅述。
一樣的,還能夠定義align_horizontal_to
。
下面的代碼經過擴展屬性來設置點擊事件:
var View.onClick: (View) -> Unit
get() { return {} } set(value) { setOnClickListener { v -> value(v) } } 複製代碼
爲View
擴展屬性onClick
,它是函數類型
。 而後就能夠像這樣設置點擊事件了:
private fun buildViewByClDsl(): 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 } } val onBackClick = { v : View -> activity?.finish() } 複製代碼
得益於函數類型
,能夠把點擊邏輯封裝在一個 lambda 中並賦值給變量onBackClick
。
RecyclerView
沒有子控件點擊事件監聽器,一樣能夠經過擴展屬性來解決這個問題:
//'爲 RecyclerView 擴展表項點擊監聽器屬性'
var RecyclerView.onItemClick: (View, Int) -> Unit get() { return { _, _ -> } } set(value) { setOnItemClickListener(value) } //'爲 RecyclerView 擴展表項點擊監聽器' fun RecyclerView.setOnItemClickListener(listener: (View, Int) -> Unit) { //'爲 RecyclerView 子控件設置觸摸監聽器' addOnItemTouchListener(object : RecyclerView.OnItemTouchListener { //'構造手勢探測器,用於解析單擊事件' val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener { override fun onShowPress(e: MotionEvent?) { } override fun onSingleTapUp(e: MotionEvent?): Boolean { //'當單擊事件發生時,尋找單擊座標下的子控件,並回調監聽器' 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) { } //'在攔截觸摸事件時,解析觸摸事件' override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { gestureDetector.onTouchEvent(e) return false } override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { } }) } 複製代碼
而後能夠像這樣爲RecyclerView
設置表項點擊事件:
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() } 複製代碼
如何獲取控件實例的引用?得益於 DSL 的語法糖,這套動態佈局構建有一種新的方法:
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 = 20 src = 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) } } 複製代碼
除了這種方式,還有一種常規方式:
fun <T : View> View.find(id: String): T = findViewById<T>(id.toLayoutId()) 複製代碼fun <T : View> AppCompatActivity.find(id: String): T = findViewById<T>(id.toLayoutId()) 複製代碼
下一篇會在 DSL 基礎上,加上數據綁定功能。
GitHub 上的代碼把上述全部的擴展方法和屬性都寫在了一個Layout.kt
文件中,在業務界面引入該文件中的全部內容後,就能在寫動態佈局時帶有補全功能(只列舉了經常使用的控件及其屬性的擴展,如有需求可自行添加。)
代碼鏈接在這
本文使用 mdnice 排版