1. 程式人生 > >自定義字型方案

自定義字型方案

在一個應用中,我需要在所有的UI元件中使用客戶提供的字型。這聽起來似乎是個很稀鬆平常的任務,不是嗎?是的,我當時也是這麼想的。然後我震驚了,Android竟然沒有提供一個簡單優雅的方式來做這件事情!

所以,在這篇文章中我會展示Android提供的預設方法,然後我會分享更加簡單優雅的解決方案。

情景

你需要為整個應用替換自定義字型。

解決方案

1)Android預設方法 #1

你可以通過ID查詢到View,然後挨個為它們設定字型。在單個View的情況下,它看起來也沒有那麼可怕。

Typeface customFont = Typeface.createFromAsset(this.getAssets(), "fonts/YourCustomFont.ttf"
); TextView view = (TextView) findViewById(R.id.activity_main_header); view.setTypeface(customFont);

但是在很多TextView、Button等文字元件的情況下,我敢肯定你不會喜歡這個方法的。:D

2)Android預設方法 #2

你可以為每個文字元件建立一個子類,如TextView、Button等,然後在建構函式中載入自定義字型。

public class BrandTextView extends TextView {

      public BrandTextView(Context context, AttributeSet attrs, int
defStyle)
{ super(context, attrs, defStyle); } public BrandTextView(Context context, AttributeSet attrs) { super(context, attrs); } public BrandTextView(Context context) { super(context); } public void setTypeface(Typeface tf, int style)
{ if (style == Typeface.BOLD) { super.setTypeface(Typeface.createFromAsset(getContext().getAssets(), "fonts/YourCustomFont_Bold.ttf")); } else { super.setTypeface(Typeface.createFromAsset(getContext().getAssets(), "fonts/YourCustomFont.ttf")); } } }

然後只需要將標準的文字控制元件替換成你自定義的就可以了(例如BrandTextView替換TextView)。

<com.your.package.BrandTextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="View with custom font"/>
<com.your.package.BrandTextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:textStyle="bold"
         android:text="View with custom font and bold typeface"/>

還有,你甚至可以直接在XML中新增自定義的字型屬性。要實現這個,你需要定義你自己的`declare-styleable`屬性,然後在元件的建構函式中解析它們。

為了不佔篇幅介紹這麼基礎的東西,這裡有一篇不錯的文章告訴你怎麼自定義控制元件屬性。

在大多數情況下,這個方法還不賴,並且有一些優點(例如,切換字型粗細等等,字型可以在元件xml檔案的typeface屬性中定義)。但是我認為這個實現方法還是太重量級了,並且依賴大量的模板程式碼,為了一個替換字型的簡單任務,有點兒得不償失。

3)我的解決方案

理想的解決方案是自定義主題,然後應用到全域性或者某個Activity。
但不幸的是,Android的`android:typeface`屬性只能用來設定系統內嵌的字型,而非使用者自定義字型(例如assets檔案中的字型)。這就是為什麼我們無法避免在Java程式碼中載入並設定字型。

所以我決定建立一個幫助類,使得這個操作儘可能的簡單。使用方法:

FontHelper.applyFont(context, findViewById(R.id.activity_root), "fonts/YourCustomFont.ttf");

並且這行程式碼會用來載入所有的基於TextView的文字元件(TextView、Button、RadioButton、ToggleButton等等),而無需考慮介面的佈局層級如何。

標準(左)與自定義(右)字型的用法。

Standard (left) and Custom (right) fonts usage.

這是怎麼做到的?非常簡單:

public static void applyFont(final Context context, final View root, final String fontName) {
    try {
        if (root instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) root;
            for (int i = 0; i < viewGroup.getChildCount(); i++)
                applyFont(context, viewGroup.getChildAt(i), fontName);
        } else if (root instanceof TextView)
            ((TextView) root).setTypeface(Typeface.createFromAsset(context.getAssets(), fontName));
    } catch (Exception e) {
        Log.e(TAG, String.format("Error occured when trying to apply %s font for %s view", fontName, root));
        e.printStackTrace();
    }
}

正如你所看到的,所需要做的僅僅是將基於TextView的文字元件從佈局中遍歷出來而已。

你可以在這裡下載到示例程式碼,裡面有`FontHelper`的具體用法。

譯者注

在多個專案中,我都碰到過類似的需求,早期採用的是第二種實現方法,但是缺點在於對於第三方元件,你需要去修改別人的程式碼,才能實現自定義字型,這恰恰違反了OC(Open & Close)原則,對擴充套件開放,對修改封閉。

剛看到第三種的時候,也是驚為天人,姑且不說結果,我覺得這種活躍的思路非常重要,很值得學習參考。

但是最後被team裡的人否決了,理由是違背元件設計原則,實現方式略嫌粗暴。後來我仔細想想,一個是要做好記憶體管理(似乎會引起記憶體問題),檢視狀態改變,也要重複載入(橫豎屏、onResume等),也絕對不是簡單的活兒。

所以暫定使用第一種方法,typeface使用單例,需要時設定字型。

我個人覺得第一種還是個體力活,而且到後來,這個程式碼重複率還是非常高的,這又違背了DRY原則。

在地鐵上的時候,突然想到DI(Dependency Inject)。已經有一些DI的框架,如ButterKnife,那寫出來應該是這樣:

@CustomFont(R.id.textView) TextView textView

or

@InjectView(id=R.id.textView, customFont=true) View anyView
@InjectView(id=R.id.textView, customFont=true, font="fonts/ltfz.ttf") View anyView

這樣寫出來程式碼相比重複寫setTypeface要好一些。

目前我們的專案還沒有使用這類DI框架,等以後引入了,使用第二種注入,寫起來應該是很爽的。

保持更新。

參考