Kotlin Android UI利器之Anko Layouts

引言

前段时间写了一篇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来写如下所示:

 verticalLayout {
                verticalLayout {
                    backgroundResource = R.mipmap.setting_about_us_bg
                    setGravity(Gravity.CENTER_HORIZONTAL)
                    imageView {
                        backgroundResource = R.mipmap.setting_about_us_logo_ic
                    }.lparams(width = wrapContent, height = wrapContent){
                        topMargin = dip(114)
                    }

                    mTvVersion = textView{
                        textSize = 14f
                        textColor = R.color.yx_text_desc
                    }.lparams(width = wrapContent, height = wrapContent){
                        topMargin = dip(9)
                        bottomMargin = dip(186)
                    }

                    verticalLayout {
                        setGravity(Gravity.CENTER_HORIZONTAL)
                        textView{
                            text = ResourcesUtil.getString(R.string.about_check_update)
                            textSize = 14f
                            backgroundResource = R.drawable.selector_about_us_btn_bg
                            textColor = R.color.yx_text_desc
                            gravity = Gravity.CENTER
                        }.lparams(width = dip(127), height = dip(36)){
                            bottomMargin = dip(20)
                        }
                        textView{
                            text = ResourcesUtil.getString(R.string.private_rights)
                            textSize = 14f
                            backgroundResource = R.drawable.selector_about_us_btn_bg
                            textColor = R.color.yx_text_desc
                            gravity = Gravity.CENTER
                        }.lparams(width = dip(127), height = dip(36)){
                        }

                        view().lparams(width = wrapContent, height = 0 , weight = 1.0f)

                        mTvCorpRight = textView{
                            text = ResourcesUtil.getString(R.string.corpright_format)
                            textColor = R.color.yx_text_desc
                        }.lparams(width = wrapContent, height = wrapContent){
                            bottomMargin = dip(20)
                        }
                    }


                }
            }
  • verticalLayout就是orientation设置为Vertical的LinearLayout
  • 布局总共分为两部分,一部分关于控件自身的属性,比如textViewtext属性。一部分是关于控件的LayoutParam,写在lparams参数中,例如margin等等,括号内定义控件的宽高值
  • 整体写法上与XML布局很相似,也是从上往下依次定义各控件

支持扩展

Anko支持扩展方法,例如我们可以做如下扩展

fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) { 
    Toast.makeText(this, message, duration).show()
}

然后我们就可以在Anko中直接用该toast方法

  verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { toast("Hello!") }
            }
        }

当然,如果括号中任何方法也没有的话可以省略括号。

verticalLayout {
    button("Ok")
    button(R.string.cancel)
}

支持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 <T : View> T.widthProcent(procent: Int): Int =
  getAppUseableScreenSize().x.toFloat().times(procent.toFloat() / 100).toInt()

有兴趣的童鞋可以用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)

Anko DSL Preview插件支持

那么我们如何才能像编写XML布局可以随时查看编写效果呢?Anko推出了Android Studio的扩展插件,装上之后也就和我们平时用XML编写布局别无二致了。

Anko DSL

性能

当然上述的写法上虽然Anko写起来更加简洁明了,但作为开发人员,我们更关注于效率和性能,那么他们之间到底有什么差别呢?
XML布局需要从资源文件中获取,然后需要用XmlPullParser解析所有的元素并一个一个的创建它们。这个过程很繁重,而且XML有很多冗余的tag,加载这些冗余的信息也加大了开销。我们来做个实验,笔者挑了严选项目一个简单的页面进行改造,发现即便这个比较简单的View用Anko与XML的时间开销的差别达到了好几倍。

XML

xml

Anko

anko

以上是同一个界面用XML实现和Anko实现的截取的三个结果,为了实验的准确性,总共用了8款机型(Meizu MX2, VIVO X5M, HUAWEI Mate 8, HUAWEI Nexus 6P, XIAOMI 2S, Galaxy Note Edge, T1, MeiZu M1),分别进行了30次测量,并对结果进行整理统计:

XML Anko
Measure 0.312ms 0.136ms
Layout 0.28ms 0.130ms
Draw 39.4ms 27.5ms

我们仅仅在这个简单的页面中就体现出近300%的速度差距,足以见得Anko在性能上的优势相较于XML更节省渲染时间。而且对于低端机型MX2,小米2S,差距尤为显著,有近500%的速度差距。

原理

那么为什么XML和Anko可以效率差距那么明显呢?我们首先来看一下Anko Layouts部分的源码,了解它的工作原理。这里以verticalLayout为例:

我们找到CustomService.kt这个类,发现有如下扩展方法

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)
}

正是由于这个扩展方法,才允许我们在Activity中使用verticalLayout,VERTICAL_LAYOUT_FACTORY即是定义orientation为vertical的工厂类factory。

继续看AnkoView这个扩展方法

inline fun <T : View> Activity.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
    val ctx = AnkoInternals.wrapContextIfNeeded(this, theme)
    val view = factory(ctx)
    view.init()
    AnkoInternals.addView(this, view)
    return view
}

这个方法很简单,主要做了如下三件事情:

  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
        }
    }

接下来划重点了,着重看一下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")
        }
    }

这里is其实就是if (manager instanceof ViewGroup),所以这里是调用了LinearLayout的addView,从ViewGroup源码可知,即将view添加到最后一个子View的后面。

将子View添加到ViewGroup之后又是怎么设置到activity的contentView的呢?我们继续往下看,在Activity的addView扩展方法中调用了createAnkoContext(activity, { AnkoInternals.addView(this, view) }, true),以下所示

   inline fun <T> T.createAnkoContext(
            ctx: Context,
            init: AnkoContext<T>.() -> Unit,
            setContentView: Boolean = false
    ): AnkoContext<T> {
        val dsl = AnkoContextImpl(ctx, this, setContentView)
        dsl.init()
        return dsl
    }

继续看实现类AnkoContextImpl,

open class AnkoContextImpl<T>(
        override val ctx: Context,
        override val owner: T,
        private val setContentView: Boolean
) : AnkoContext<T> {
    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. 判断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的优势主要在于:

  1. DSL减少了XML解析的时间及内存开销,加快了渲染效率。
  2. DSL更简洁易读,减少了XML冗余的tag信息。
  3. DSL扩展性更强,支持扩展方法。
  4. DSL复用性更好,相比include方式更灵活。
  5. 在动态布局方面更有优势,避免了复杂的判断逻辑。

当然缺点也有如下几点:

  1. 有一定的学习成本
  2. Anko DSL Preview插件对于AS 2.2以上支持还有点问题。

当然这些缺点都不算什么,既然有Google的支持,未来趋势Kotlin所占的份额肯定是越来越多,Anko也在不断完善中,以上文章如有写错的地方欢迎拍砖,文明交流。

参考文章

  1. Kotlin docs
  2. kotlin-for-android-developers
  3. Anko Layout wiki
  4. Use Kotlin Anko DSL and Say No to Android Layouts Written in XML
  5. Building a UI with Kotlin and Anko
  6. 400% faster layouts with Anko
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容