引言
前段时间写了一篇Kotlin语法入门的文章,还没有看过的盆友请戳(这里),有的可能看完之后已经开始尝试用kotlin来写代码了。不过上篇体现的仅仅是针对于Kotlin相较于Java在用法上的扩展性以及写法上的简洁性,那么Android中还有另一个重要的组成部分,布局文件呢?接下来我们就继续看一下Anko(基于Kotlin的扩展库)对于Android传统布局文件XML做的改进及优化,以及工作原理。
定义
Anko是Kotlin为Android推出的第三方库,旨在提升Android界面的开发效率,使代码更简洁易懂并更容易阅读。Anko总共分为以下四个部分:
- Anko Commons: 轻量级类库包括intent,dialog,logging等帮助类
- Anko Layouts:快速的空安全的方式来写动态的布局
- Anko SQLite:关于Android SQLite查询语句DSL和容器解析器
- Anko Coroutines:Coroutines提供了一种长时间阻塞线程的解决方案,并且代之以开销更小和更可控的操作(suspension of a coroutine)
我们可以看到,Anko不仅仅可以用来写布局,更加可以做一些基础支持工具,比如操作数据库,用Intent进行数据传递等等,本文着重探讨的是Anko Layouts这一部分。
优势
- Anko可以让我们在源码中写UI布局,严格的编译检查可以保证类型安全,不会出现类型转换异常
- 没有多余的CPU开销来解析XML文件
- 我们可以把Anko DSL约束放在函数中,提高代码复用率,比原有xml的include更强大
用法
如下,是应用中的关于我们界面布局文件:
由于布局非常简单,就不多解释了,那么如果将上述布局用Anko来写如下所示:
1 | verticalLayout { |
- verticalLayout就是orientation设置为Vertical的LinearLayout
- 布局总共分为两部分,一部分关于控件自身的属性,比如
1
2
3
4
5* 整体写法上与XML布局很相似,也是从上往下依次定义各控件
## 支持扩展
Anko支持扩展方法,例如我们可以做如下扩展
fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}1
2
然后我们就可以在Anko中直接用该toast方法
verticalLayout {
val name = editText()
button(“Say Hello”) {
onClick { toast(“Hello!”) }
}
}1
当然,如果括号中任何方法也没有的话可以省略括号。
verticalLayout {
button(“Ok”)
button(R.string.cancel)
}1
2
3
4
5
6
7
## 支持Runtime Layouts
如果你有在特定逻辑下才会出现的布局,那么使用Anko来实现就很方便了,而如果用原有的方式,就必须在Java代码里编写布局,而相较于Anko来实现会显得冗余而且难以维护,尤其遇到复杂的布局实现,纯粹使用Java代码去写会非常头疼。
例如要实现一个只有在横屏情况下,且横屏最小宽度要大于700px,才会展示一个特定的宽度的RecyclerView,宽度为屏幕宽度的50%。
用Anko DSL来实现,只需10行代码。
configuration(orientation = Orientation.LANDSCAPE, smallestWidth = 700) {
recyclerView {
init()
}.lparams(width = widthProcent(50), height = matchParent)
frameLayout().lparams(width = matchParent, height = matchParent)
}
fun
getAppUseableScreenSize().x.toFloat().times(procent.toFloat() / 100).toInt()1
2
3
4
5
6
7
8
有兴趣的童鞋可以用Java代码来实现这个布局,并且与以上代码进行对比。
## 适配不同SDK版本更方便
如上述代码那样,用Anko来写布局和XML没有什么两样。但由于Android碎片化问题比较严重,不同版本的SDK占有率相差不大,为了针对不同SDK版本的手机有更优的体验,我们就需要对不同的SDK版本进行最新API的适配。
用Anko来编写布局使得我们可以进行兼容性检查,根据SDK的版本来使用哪种API,而不是在布局文件中来写两个XML文件。例如当SDK版本大于5.0才会设置elevation属性:
appBarLayout {
toolBar = toolbar {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) elevation = 4f
}.lparams(width = matchParent, height = actionBarSize())
}.lparams(width = matchParent)
1 |
|
inline fun Activity.verticalLayout(theme: Int = 0): LinearLayout = verticalLayout(theme) {}
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView($$Anko$Factories$CustomViews
.VERTICAL_LAYOUT_FACTORY, theme, init)
}1
2
3
4
正是由于这个扩展方法,才允许我们在Activity中使用verticalLayout,VERTICAL_LAYOUT_FACTORY即是定义orientation为vertical的工厂类factory。
继续看AnkoView这个扩展方法
inline fun
val ctx = AnkoInternals.wrapContextIfNeeded(this, theme)
val view = factory(ctx)
view.init()
AnkoInternals.addView(this, view)
return view
}1
2
3
4
5
6
7
8
9这个方法很简单,主要做了如下三件事情:
1. 工厂类将子view提取出来
2. 初始化提取出来的子view
3. 将view添加至root view上,这里是LinearLayout
```AnkoInternals```是Anko核心类,提供了许多核心方法,其中就有涉及布局的addView方法,稍后会介绍。首先看
```wrapContextIfNeeded```这个方法
fun wrapContextIfNeeded(ctx: Context, theme: Int): Context {
return if (theme != 0 && (ctx !is AnkoContextThemeWrapper || ctx.theme != theme)) {
// 如果该context不是ContextThemeWrapper或它的子类且theme不为0,将对其进行包装,使其成为AnkoContextThemeWrapper继承自ContextThemeWrapper。
AnkoContextThemeWrapper(ctx, theme)
} else {
ctx
}
}1
2
接下来划重点了,着重看一下```AnkoInternals.addView(this, view)```:
fun <T : View> addView(manager: ViewManager, view: T) {
return when (manager) {
is ViewGroup -> manager.addView(view)
is AnkoContext<*> -> manager.addView(view, null)
else -> throw AnkoException("$manager is the wrong parent")
}
}
1 |
|
inline fun
ctx: Context,
init: AnkoContext
setContentView: Boolean = false
): AnkoContext
val dsl = AnkoContextImpl(ctx, this, setContentView)
dsl.init()
return dsl
}1
2
继续看实现类AnkoContextImpl,
open class AnkoContextImpl
override val ctx: Context,
override val owner: T,
private val setContentView: Boolean
) : AnkoContext
private var myView: View? = null
override val view: View
get() = myView ?: throw IllegalStateException("View was not set previously")
override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
if (view == null) return
if (myView != null) {
alreadyHasView()
}
this.myView = view
if (setContentView) {
doAddView(ctx, view)
}
}
private fun doAddView(context: Context, view: View) {
when (context) {
is Activity -> context.setContentView(view)
is ContextWrapper -> doAddView(context.baseContext, view)
else -> throw IllegalStateException("Context is not an Activity, can't set content view")
}
}
open protected fun alreadyHasView(): Unit = throw IllegalStateException("View is already set: $myView")
}1
2
3
4
5
6
7
8
9
10
11
主要做了以下几个事情:
1. 判断view是不是为空,为空则直接返回
2. 判断view是不是已经设置过,如果已设置会抛出异常
3. 判断setContentView是否为true,为true,则会调用Activity的```setContentView(view)```方法。
所以到这里我们就把Anko DSL的工作流程基本上讲完了,那么可以看到,Anko在解析时间上节省了XML解析的开销,接下来我们来对比一下Android加载XML布局的方式。
我们知道,Android可以通过LayoutInflater.inflate方法来加载布局文件到内存中,由于本文着重介绍的是Anko DSL,这里简单列出关键的rInflate代码
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
…
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
...
}
`
通过分析源码,不难发现,主要是使用XmlPullParser通过循环解析xml文件并将信息解析到内存View对象,布局文件中定义的一个个组件都被顺序的解析到了内存中并被父子View的形式组织起来。
总结
结合上面的分析,我们不难总结出Anko Layouts相较于XML的优势主要在于:
- DSL减少了XML解析的时间及内存开销,加快了渲染效率。
- DSL更简洁易读,减少了XML冗余的tag信息。
- DSL扩展性更强,支持扩展方法。
- DSL复用性更好,相比include方式更灵活。
- 在动态布局方面更有优势,避免了复杂的判断逻辑。
当然缺点也有如下几点:
- 有一定的学习成本
- Anko DSL Preview插件对于AS 2.2以上支持还有点问题。
当然这些缺点都不算什么,既然有Google的支持,未来趋势Kotlin所占的份额肯定是越来越多,Anko也在不断完善中,以上文章如有写错的地方欢迎拍砖,文明交流。