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

xml1xml2xml3

Anko

anko1 anko2 anko3

以上是同一個界面用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
Layout 0.28ms
Draw 39.4ms

我們僅僅在這個簡單的頁面中就體現出近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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章