Kotlin 開發Android 實戰(二)- 界面構建與擴展方法
這是實戰Kotlin@Android的第二部分,如果你還沒讀過第一部分,建議先閱讀第一部分《Kotlin 開發Android 實戰(一)》
在前面的文章中我們使用Kotlin中type-safe builder模式寫了一個還算有用的v方法,它可以構建任意Android View實例。
import android.content.Context
import java.lang.reflect.Constructor
inline fun <reified TV : View> v(context: Context, init: TV.() -> Unit) : TV {
val constr = TV::class.java.getConstructor(Context::class.java)
val view = constr.newInstance(context)
view.init()
return view
}
我們可以在其他Kotlin代碼中調用這個方法來創建并初始化任何類型的View:
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.TextView
val view = v<TextView>(context) {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
text = "Hello"
}
這真的實用嗎?
現在我們要創建一個很簡單的layout,它包含兩個TextView。在XML可以這樣表示:
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />
</LinearLayout>
我們可憐的v方法不能一下子創建這么多,不過只需借助一點幫助。我們需要再寫一個能夠將View添加至父View(LinearLayout, RelativeLayout)的方法。我們現在寫一個新的v方法。
inline fun <reified TV : View> v(parent: ViewGroup, init: TV.() -> Unit) : TV {
val constr = TV::class.java.getConstructor(Context::class.java)
val view = constr.newInstance(parent.context)
parent.addView(view)
view.init()
return view
}
這個方法與原本的v方法幾乎一摸一樣,區別只在第一個參數上:不再是Context, 變成了ViewGroup類型。這個新的v方法需要持有父ViewGroup,以便將新創建的View對象在初始化并返回之前添加進其中。而新的v方法又能通過ViewGroup來獲取Context以初始化View,這樣就不用再傳入Context對象了。
現在我們看一下新的v方法如何與舊的協作來構建上述View層級。
import android.content.Context
import android.widget.LinearLayout
import android.widget.LinearLayout.VERTICAL
import android.widget.LinearLayout.LayoutParams
import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
import android.widget.TextView
val view = v<LinearLayout>(context) {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
orientation = VERTICAL
// adds a new TextView as the first child to the LinearLayout ("this")
v<TextView>(this) {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
text = "Hello"
}
// adds a another new TextView to the LinearLayout
v<TextView>(this) {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
text = "World"
}
}
這里Kotlin代碼會像XML一樣嵌套,非常好看。
在上一部分中說過,Kotlin lambda with receiver可以在lambda內部以this關鍵字引用receiver對象。在上面的例子中,在外部v的lambda中receiver是LinearLayout,它作為第一個參數被傳入了兩個內部v方法(剛寫的v方法)。因為LinearLayout是ViewGroup的子類,Kotlin知道我們在調用新寫的v方法,因為舊的需要傳入Context。
通過這兩個兄弟v方法我們可以動態地、精確地創建嵌套View,其中的ViewGroup和View的具體類型均無限制。現在我們已經可以發現這種表述性的創建方式與XML有些相似,而在后續的文章中,我們也將發現Kotlin的速度要快一些。
提升空間
Kotlin的type-safe builder模式起了很大的作用,但是在很多時候,Kotlin還是比XML復雜不少。比如在Kotlin中當我們想設置一個TextView的maxWidth屬性為120dp時:
val view = v<TextView>(context) {
// 丑
maxWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 120, context.resources.displayMetrics).toInt()
}
而在XML中,只需要:
<TextView android:maxWidith="120dp" />
本來是為了簡化工作寫的v方法一下變麻煩了。
這里需要將dp轉化為px的簡便方法
我在這里想要一個方法可以將dp轉化為像素,然后上面的代碼最好能長這樣:
val view = v<TextView>(context) {
// simpler way to set maxWidth to 120dp
maxWidth = dp_i(120)
}
這里的方法可以接收一個以dp為單位的值,然后返回當前設備下轉化成像素的值。不過為什么要叫這個方法dp_i而不是dp呢?在Android中有時會返回float而有時會返回int,我也不想再自己進行轉換,所以就給兩種返回類型都寫一個方法:”dp_i”和“dp_f”。
但在這里仍有問題。如果你看一下剛才很丑的那段代碼,會發現計算像素值時需要Context對象。我可不想每次調用dp_i方法都傳入Context作為參數,所以在這里要用到Kotlin的另一個技能: extension functions擴展方法 。讓我們直接看一下擴展方法長什么樣:
import android.view.View
fun View.dp_f(dp: Float): Float {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, context.resources.displayMetrics)
}
fun View.dp_i(dp: Float): Int {
return dp_f(dp).toInt()
}
擴展方法如何工作?
你可能注意到的第一個點就是方法的前綴。你可能本以為第一個方法應該是dp_f,結果是View.dp_f。這是Kotlin中針對擴展方法的一個特殊語法。這里將一個類名和一個方法名以點連接,而意思就是告訴Kotlin我們要給View類添加兩個新方法”dp_i”,”dp_f”。這樣使用擴展方法有幾點好處:
第一,在擴展方法內作為類的成員可以訪問其成員變量和方法(只有public和internal)。也就是說dp_f可以通過View內部的context屬性來訪問其Context引用。現在我們不需要將Context作為參數傳入了,因為它隱含在View中。
第二,在導入了(import)這些擴展方法的代碼段中可以像調用一個對象的普通方法一樣調用其擴展方法。在這里,在v方法的lambda with receiver代碼塊中可以通過receiver View對象直接調用這些方法,像這樣:maxWidth = dp_i(120),Kotlin會識別出需要調用View類型的receiver對象的dp_i方法。
值得注意的一點是,Kotlin在聲明擴展方法時,不會修改其class。所以在這里,View類的其他方法不能訪問擴展方法,因為擴展方法不是真正意義上的成員。
現在我們就有將dp轉成px的簡便方法了。
擴展方法還有其他的用處。現在我們已經看到通過擴展方法可以簡化一些棘手的代碼,我們利用這一點繼續簡化v方法。
現在我們有兩個v方法,第一個用于構建根元素,接收Context,第二個用于創建嵌套于父View中的子View。
inline fun <reified TV : View> v(context: Context, init: TV.() -> Unit) : TV
inline fun <reified TV : View> v(parent: ViewGroup, init: TV.() -> Unit) : TV
如果我們不需要傳入Context或是ViewGroup作為參數,豈不是很好?通過擴展方法,我們就像剛才避免將Context傳入dp_f一樣重構這段代碼。下面使用擴展方法重新實現兩個v方法,注釋是兩個方法原本的聲明。
//inline fun <reified TV : View> v(context: Context, init: TV.() -> Unit) : TV {
inline fun <reified TV : View> Context.v(init: TV.() -> Unit) : TV {
val constr = TV::class.java.getConstructor(Context::class.java)
// val view = constr.newInstance(context)
val view = constr.newInstance(this)
view.init()
return view
}
//inline fun <reified TV : View> v(parent: ViewGroup, init: TV.() -> Unit) : TV {
inline fun <reified TV : View> ViewGroup.v(init: TV.() -> Unit) : TV {
val constr = TV::class.java.getConstructor(Context::class.java)
// val view = constr.newInstance(parent.context)
val view = constr.newInstance(context)
parent.addView(view)
view.init()
return view
}
你可以看到我們去掉了兩個方法的第一個參數(Context和ViewGroup),并通過所繼承的類來獲取所需實例的引用。現在這兩個方法都只有一個參數:用于修改View的lambda with recceiver。
修改了方法后,如果我們在Activity(Context子類)中寫代碼,那就可以將v添加做Activity對象的成員。這樣我們就可以以這樣更簡單的方式構建嵌套View。
v<LinearLayout> {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
orientation = VERTICAL
v<TextView> {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
text = "Hello"
}
v<TextView> {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
text = "World"
}
}
這里調用v方法根本不像是在調用方法,因為我們不需要圓括號。在第一部分中我說過,如果方法的最后一個參數是lambda,那就可以放在圓括號后,而在這里,只有一個參數,根本就不用寫圓括號。
Kotlin中的擴展方法幫我們在代碼中很簡明易懂地創建構建View層級。不過還是有其他問題需要注意。比如我們想設置TextView的左內邊距為16dp。
v<TextView> {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
setPadding(dp_i(16), 0, 0, 0) // bleh - it's not consistent
text = "Hello"
}
在這里調用setPadding()方法與直接修改屬性放在一起真是挺丑的,之所以有這樣的情況發生,是因為setPadding()方法有多個參數,并不是一個JavaBean風格的Setter方法。所以,Kotlin不能為其制定一個虛擬屬性。不用怕,我會在后續文章中通過Kotlin的另外一個功能來彌補這個問題。
來自: http://blog.chengdazhi.com/index.php/162