Kotlin Android UI利器之Anko Layouts

引言

前段時間寫了一篇Kotlin語法入門的文章,還沒有看過的盆友請戳(這里),有的可能看完之后已經(jīng)開始嘗試用kotlin來寫代碼了。不過上篇體現(xiàn)的僅僅是針對于Kotlin相較于Java在用法上的擴(kuò)展性以及寫法上的簡潔性,那么Android中還有另一個重要的組成部分,布局文件呢?接下來我們就繼續(xù)看一下Anko(基于Kotlin的擴(kuò)展庫)對于Android傳統(tǒng)布局文件XML做的改進(jìn)及優(yōu)化,以及工作原理。

定義

Anko是Kotlin為Android推出的第三方庫,旨在提升Android界面的開發(fā)效率,使代碼更簡潔易懂并更容易閱讀。Anko總共分為以下四個部分:

  • Anko Commons: 輕量級類庫包括intent,dialog,logging等幫助類
  • Anko Layouts:快速的空安全的方式來寫動態(tài)的布局
  • Anko SQLite:關(guān)于Android SQLite查詢語句DSL和容器解析器
  • Anko Coroutines:Coroutines提供了一種長時間阻塞線程的解決方案,并且代之以開銷更小和更可控的操作(suspension of a coroutine)

我們可以看到,Anko不僅僅可以用來寫布局,更加可以做一些基礎(chǔ)支持工具,比如操作數(shù)據(jù)庫,用Intent進(jìn)行數(shù)據(jù)傳遞等等,本文著重探討的是Anko Layouts這一部分。

優(yōu)勢

  • Anko可以讓我們在源碼中寫UI布局,嚴(yán)格的編譯檢查可以保證類型安全,不會出現(xiàn)類型轉(zhuǎn)換異常
  • 沒有多余的CPU開銷來解析XML文件
  • 我們可以把Anko DSL約束放在函數(shù)中,提高代碼復(fù)用率,比原有xml的include更強(qiáng)大

用法

如下,是應(yīng)用中的關(guān)于我們界面布局文件:

關(guān)于我們

由于布局非常簡單,就不多解釋了,那么如果將上述布局用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設(shè)置為Vertical的LinearLayout
  • 布局總共分為兩部分,一部分關(guān)于控件自身的屬性,比如textViewtext屬性。一部分是關(guān)于控件的LayoutParam,寫在lparams參數(shù)中,例如margin等等,括號內(nèi)定義控件的寬高值
  • 整體寫法上與XML布局很相似,也是從上往下依次定義各控件

支持?jǐn)U展

Anko支持?jǐn)U展方法,例如我們可以做如下擴(kuò)展

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

當(dāng)然,如果括號中任何方法也沒有的話可以省略括號。

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

支持Runtime Layouts

如果你有在特定邏輯下才會出現(xiàn)的布局,那么使用Anko來實(shí)現(xiàn)就很方便了,而如果用原有的方式,就必須在Java代碼里編寫布局,而相較于Anko來實(shí)現(xiàn)會顯得冗余而且難以維護(hù),尤其遇到復(fù)雜的布局實(shí)現(xiàn),純粹使用Java代碼去寫會非常頭疼。

例如要實(shí)現(xiàn)一個只有在橫屏情況下,且橫屏最小寬度要大于700px,才會展示一個特定的寬度的RecyclerView,寬度為屏幕寬度的50%。

用Anko DSL來實(shí)現(xiàn),只需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代碼來實(shí)現(xiàn)這個布局,并且與以上代碼進(jìn)行對比。

適配不同SDK版本更方便

如上述代碼那樣,用Anko來寫布局和XML沒有什么兩樣。但由于Android碎片化問題比較嚴(yán)重,不同版本的SDK占有率相差不大,為了針對不同SDK版本的手機(jī)有更優(yōu)的體驗,我們就需要對不同的SDK版本進(jìn)行最新API的適配。
用Anko來編寫布局使得我們可以進(jìn)行兼容性檢查,根據(jù)SDK的版本來使用哪種API,而不是在布局文件中來寫兩個XML文件。例如當(dāng)SDK版本大于5.0才會設(shè)置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插件支持

那么我們?nèi)绾尾拍芟窬帉慩ML布局可以隨時查看編寫效果呢?Anko推出了Android Studio的擴(kuò)展插件,裝上之后也就和我們平時用XML編寫布局別無二致了。

Anko DSL

性能

當(dāng)然上述的寫法上雖然Anko寫起來更加簡潔明了,但作為開發(fā)人員,我們更關(guān)注于效率和性能,那么他們之間到底有什么差別呢?
XML布局需要從資源文件中獲取,然后需要用XmlPullParser解析所有的元素并一個一個的創(chuàng)建它們。這個過程很繁重,而且XML有很多冗余的tag,加載這些冗余的信息也加大了開銷。我們來做個實(shí)驗,筆者挑了嚴(yán)選項目一個簡單的頁面進(jìn)行改造,發(fā)現(xiàn)即便這個比較簡單的View用Anko與XML的時間開銷的差別達(dá)到了好幾倍。

XML

xml

Anko

anko

以上是同一個界面用XML實(shí)現(xiàn)和Anko實(shí)現(xiàn)的截取的三個結(jié)果,為了實(shí)驗的準(zhǔn)確性,總共用了8款機(jī)型(Meizu MX2, VIVO X5M, HUAWEI Mate 8, HUAWEI Nexus 6P, XIAOMI 2S, Galaxy Note Edge, T1, MeiZu M1),分別進(jìn)行了30次測量,并對結(jié)果進(jìn)行整理統(tǒng)計:

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

我們僅僅在這個簡單的頁面中就體現(xiàn)出近300%的速度差距,足以見得Anko在性能上的優(yōu)勢相較于XML更節(jié)省渲染時間。而且對于低端機(jī)型MX2,小米2S,差距尤為顯著,有近500%的速度差距。

原理

那么為什么XML和Anko可以效率差距那么明顯呢?我們首先來看一下Anko Layouts部分的源碼,了解它的工作原理。這里以verticalLayout為例:

我們找到CustomService.kt這個類,發(fā)現(xiàn)有如下擴(kuò)展方法

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

正是由于這個擴(kuò)展方法,才允許我們在Activity中使用verticalLayout,VERTICAL_LAYOUT_FACTORY即是定義orientation為vertical的工廠類factory。

繼續(xù)看AnkoView這個擴(kuò)展方法

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,將對其進(jìn)行包裝,使其成為AnkoContextThemeWrapper繼承自ContextThemeWrapper。
            AnkoContextThemeWrapper(ctx, theme)
        } else {
            ctx
        }
    }

接下來劃重點(diǎn)了,著重看一下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其實(shí)就是if (manager instanceof ViewGroup),所以這里是調(diào)用了LinearLayout的addView,從ViewGroup源碼可知,即將view添加到最后一個子View的后面。

將子View添加到ViewGroup之后又是怎么設(shè)置到activity的contentView的呢?我們繼續(xù)往下看,在Activity的addView擴(kuò)展方法中調(diào)用了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
    }

繼續(xù)看實(shí)現(xiàn)類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是不是已經(jīng)設(shè)置過,如果已設(shè)置會拋出異常
  3. 判斷setContentView是否為true,為true,則會調(diào)用Activity的setContentView(view)方法。

所以到這里我們就把Anko DSL的工作流程基本上講完了,那么可以看到,Anko在解析時間上節(jié)省了XML解析的開銷,接下來我們來對比一下Android加載XML布局的方式。

我們知道,Android可以通過LayoutInflater.inflate方法來加載布局文件到內(nèi)存中,由于本文著重介紹的是Anko DSL,這里簡單列出關(guān)鍵的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);
            }
        }
            ...
        }

通過分析源碼,不難發(fā)現(xiàn),主要是使用XmlPullParser通過循環(huán)解析xml文件并將信息解析到內(nèi)存View對象,布局文件中定義的一個個組件都被順序的解析到了內(nèi)存中并被父子View的形式組織起來。

總結(jié)

結(jié)合上面的分析,我們不難總結(jié)出Anko Layouts相較于XML的優(yōu)勢主要在于:

  1. DSL減少了XML解析的時間及內(nèi)存開銷,加快了渲染效率。
  2. DSL更簡潔易讀,減少了XML冗余的tag信息。
  3. DSL擴(kuò)展性更強(qiáng),支持?jǐn)U展方法。
  4. DSL復(fù)用性更好,相比include方式更靈活。
  5. 在動態(tài)布局方面更有優(yōu)勢,避免了復(fù)雜的判斷邏輯。

當(dāng)然缺點(diǎn)也有如下幾點(diǎn):

  1. 有一定的學(xué)習(xí)成本
  2. Anko DSL Preview插件對于AS 2.2以上支持還有點(diǎn)問題。

當(dāng)然這些缺點(diǎn)都不算什么,既然有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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內(nèi)容