一種巧妙的drawable.xml替代方案

linjiang發表於2018-09-10

如何維護(替換)drawable xml是android開發中一個老生常談的話題。按照標準的Android佈局開發模式,我們不得不為各種UI效果新建不同的xml檔案進行描述,哪怕是簡單的一個圓角。隨著專案迭代,成百上千的xml連同那模稜兩可的檔名,不僅讓開發者複用或清理的成本難以估計,還使得專案體積急劇增大。因此,下面我們探索一種原理巧妙、適配全面的drawable替代方案。

傳統方案總結

我們先概括下目前市面上已有的方案,大致分為兩種實現方式。

一種是繼承某個(或某幾個)常用的控制元件,然後將drawable.xml中的常用屬性作為當前控制元件的自定義屬性,最後在控制元件內部動態生成drawable作為該控制元件的背景。這種方案的優點很明顯:能直觀地將drawable效果描述作為控制元件的屬性定義在佈局xml中,具有很好的可讀性;但是缺點也不可忽視,這些屬性並不能應用到任意控制元件,導致在很多時候還是不得不建立drawable.xml檔案。

另一種方案則是將drawable的常用屬性封裝為程式碼API,以動態的方式在程式碼中生成並賦值給控制元件。這種方案理論上完全拋棄了drawable.xml,可以適配任意控制元件,但是若想完全以這種方式達到完全替換xml,個人覺得不可能,程式碼量大,關聯性低是其最大的缺點,單看佈局,無從知曉該控制元件的最終效果。不過,如果兩相結合,作為對第一種方案的補充倒是一個不錯的方案。

新方案探索

上述兩種方案各有千秋,但都無法完全解決問題,我們對上述兩種方案進行分析,提出以下問題:為什麼不能有一種「既具有高可讀性,又能全面適配」的drawable.xml替代方案呢?也就是說能同時兼顧前面提到的兩種方案的優點,高可讀性意味著對drawable的描述需要作為屬性定義在佈局檔案中、全面適配意味這些屬性對任意控制元件都有效。思來想去,答案似乎只有一個:DataBinding。說到這裡,可能有些朋友已經隱隱猜到了,不過別急,容我娓娓道來。

DataBinding是Android官方推出的資料繫結庫,儘管已有數年,但是我估計仍有部分開發者還沒有接觸甚至有些牴觸,具體就不細說,但是我希望你暫且能擁抱它,繼續閱讀。
資料繫結讓資料變化能直接反映到佈局中,對於控制元件已有的屬性,例如TextView的android:text屬性,一旦通過DataBinding繫結:

<TextView
    android:text="@{name}"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
複製程式碼

在執行時內部就會呼叫TextView內部的setText方法。其實現原理的關鍵就是DataBinding通過提供的@BindingAdapter註解,該註解將任意指定的屬性和任意指定的方法關聯,DataBinding會在編譯的時候動態生成的呼叫關係,而對於常用的控制元件,DataBinding已經預置了對應的註解方法,例如以下就是TextView的setText方法:

@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
    final CharSequence oldText = view.getText();
    if (text == oldText || (text == null && oldText.length() == 0)) {
        return;
    }
    if (text instanceof Spanned) {
        if (text.equals(oldText)) {
            return; // No change in the spans, so don't set anything.
        }
    } else if (!haveContentsChanged(text, oldText)) {
        return; // No content changes, so don't set anything.
    }
    view.setText(text);
}
複製程式碼

我們需要關注的就是這個@BindingAdapter註解,「任意指定的屬性」這個屬性並非特指我們在佈局中Android提供的標準屬性,也就是說,我們可以提供任意字串作為屬性,而任意方法很好理解,上面的程式碼片段很好的表達了這個意思,我們唯一需要關注的就是這個方法的引數:第一個引數是指定註解中的屬性的作用域,後面的引數則是和註解所宣告的屬性一一對應,那麼結合到我們本文的主題,答案也就呼之欲出了:

新方案實現

提供一個用@BindingAdapter註解的方法,作用域指定為View(即任意控制元件);引數約定為drawable.xml中的屬性,不就達到了目的嗎。是否是感覺到一絲絲巧妙?既然方案有了,下面我們來看具體實現。

限於drawable屬性的豐富性,本文以常用的屬性solid 和 corner為例展開。如以下片段所示:

@BindingAdapter(value = {
        "drawable_solidColor",
        "drawable_radius",
}, requireAll = false)
public static void setViewBackground(View v, int color, int radius) {
    GradientDrawable drawable = new GradientDrawable();
    drawable.setColor(color);
    drawable.setCornerRadius(radius);
    view.setBackground(drawable);
}

複製程式碼

上面程式碼片段定義了兩個屬性:drawable_solidColor, drawable_radius,分別表示solid的color和corner的radius屬性,也就是說稍後我們就就可以在佈局檔案中為每個View都指定該屬性了;

這裡可能有朋友會產生疑問,drawable的屬性那麼多,這裡只定義了兩個還好,如果把所有的drawable屬性都定義,那豈不是每個控制元件都要把每個屬性都指定一次,即使不需要。所以還需要提一下requireAll引數,它表示是否需要每個屬性都必須繫結了資料才會呼叫setViewBackground方法,設定為false後,就可以在佈局檔案中只指定需要的屬性即可。

以上幾行程式碼完成了基本定義,下面我們來看看如何使用:

<layout>
    <TextView
        drawable_radius="@{10}"
        drawable_solidColor="@{0xffff0000}"
        
        android:layout_width="60dp"
        android:layout_height="60dp" />
<layout/>
複製程式碼

不用懷疑,就是這麼簡單,即使這裡不貼出效果圖,我想大家腦海中已經浮現出來了,是不是覺得一目瞭然?以此類推,其它的drawable屬性也可以通過本方案逐一實現。

總結

回顧本文,並沒有任何複雜的程式碼或高深的邏輯組合,僅提出一種巧妙的drawable.xml替代方案,具有「既具有高可讀性,又能全面適配」的特點。

從成本來說,本方案應該是最低的(特別是對一些已經在使用DataBinding的專案):只需要定義一個方法即可,而效果卻是最優的:理論來講,實現該方案後,可以減少99%的drawable.xml建立。 如果非要說出本方案的缺點,那麼它的實現原理所依賴的核心庫DataBinding可能是有些開發者所不能接受的。

讀到這裡,是否覺得意猶未盡?沒錯,我已依據本文的方案替大家整理好了幾乎所有常用的drawable屬性提交到了GitHub,核心依然是隻有一個方法,直接可用。

Github地址:github.com/whataa/noDr…

相關文章