原文链接:https://medium.com/@CodingDoug/kotlin-android-a-brass-tacks-experiment-part-4-4b7b501fa457#.v36hl6ncb
这是实战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转化为像素,然后上面的代码最好能长这样:
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的另外一个功能来弥补这个问题。
欢迎关注我的公众号,将零碎时间都用在刷干货上!