『我的App開發之路』使用 Material Design Icon 美化你的設計

Kevin Chung
10 min readMar 18, 2020

--

剛開始學習 App 的時候,UI上幾乎沒有任何美工或是圖示,按鈕選單上都只有文字,先讓人知道這個按鈕是要幹嘛的就好,等到功能慢慢齊備之後,開始無法忍受這麼陽春的設計,而且一堆的文字看起來很雜亂,便就開始上網尋找一些小圖示。

網路上的資源其實超多,反倒讓我多花了不少時間嘗試與改版,但一直沒有遇到讓我覺得完全滿意的,原因包含:

  • 不夠豐富,一開始覺得很喜歡,要最後要用的時候總缺那幾個
  • 不支援向量,一放大馬賽克就跑出來了
  • 顏色無法調整以符合主題
  • 需要額外後製
  • 需要一個一個下載
  • 太貴
  • ..

終於有一次在改版 App 按鈕的時候,在 github 找到一個 Fancy Button 的 library,它除了有豐富的外觀調整外,還有內建的圖示,只要在 xml 檔裡指定 unicode ,就可以在按鈕上顯示各種美觀的圖示,不僅可以調整大小能設定顏色。

image from fancybuttons

Font icon

這也太方便了吧!立馬就來研究一下這到底怎麼做到的,直接到 github 裡看看它的原始碼 ,原來它顯示圖示的元件就只是一個一般的 TextView,差別在於它建立的時後,將 typeface 指定為 Icon Font 類型的字型,Icon Font 與一般字型差異在於,字碼對應到的不是文字,而是自訂的圖示,使用時是透過指定 unicode 來取得特定圖示。

以這樣的方式來呈現圖示,不僅圖示可以指定顏色,還能夠無損放大/縮小,另外,也不需要像圖片方式一個一個下載,按照不同 dpi 目錄存放命名,用在操作介面上的提示上可以說非常便利。從此之後,我就不用再苦苦尋覓圖片類型的小圖示了。

FancyButtons 內建採用的圖示字型是 Font Awesome,這個字型一開始是免費的,有上千個圖示,設計具有一致性,重點是有很多的商業 logo,如 Facebook、Google、還有信用卡發行機構的logo、貨幣符號等。這對我的記帳 App 非常有用,因此這個字型我也用了蠻久的。

後來它開始改版,設計變得更優雅了,不過也變成需要付費才能使用premium 的圖示。同時我也發現 Google 也有自己針對 Material Design 設計的 Material Design Icons,圖示的設計更加貼合 Android 系統,圖示更加豐富而且持續更新(目前有超過4000個),於是便搬家到 Material Design Icons了。

Material Design Icons (MDI)

Material Design Icons 網站上下載整個 package 的壓縮檔,解開後可以在根目錄下找到 preview.html 檔,在瀏覽器中打開之後,是所有圖示的預覽以及對應的 unicode 表格。

使用的時候可以在表格中先找到需要的圖示,然後查詢它的 unicode ,例如要顯示撲克牌,可以在第一列的第二行找到它,unicode 為 f63a,程式碼如下:

myTextView.setText("&#xf63a");

不過像 “&#f63a” 這樣的字串並不太直覺,不僅不好輸入,而且日後在檢查的時候也無法從字串得到一些提示。

MDI 字型對網站的支援度很好,不僅可以透過 CDN 取得字型,還有建立好的 CSS 檔可以套用很多格式,重點是可以在 tag 的 class 中直接輸入文字標籤(ex. .mdi-access-point)套用,可惜沒有提供 android 用的 string XML 檔。

還好要產生 string XML 並不困難,透過對 CSS 內容做一些改變即可,我寫了一個 javascript 透過 node 執行來產生 XML,執行完畢就可以得到 Android 的 string table 。

var rl = require('readline').createInterface({
input: require('fs').createReadStream(process.env.FILE)
});
var state = 0;
var buf = "";
/*
.mdi-access-point:before {
content: "\F002";
}
*/
console.log('<?xml version="1.0" encoding="utf-8"?>\n<resources>')rl.on('line', function(line) {
switch(state) {
case 0:
if(line.indexOf(".mdi-")==0 && line.indexOf(":") >0) {
buf = "<string name=\""+line.substring(1,line.indexOf(":")).replace(/-/g,"_")+"\"> ";
state = 1;
}
break;
case 1: {
if(line.indexOf("content") > 0) {
line = line.substring(line.indexOf("\\")+1);
if(line.length >=6
&& !line.startsWith('FFF')) { // &#xFFFx will cause error in Android xml
line = "&#x"+line.substring(0,line.indexOf('"'))+';'+" </string> ";
buf += line;
console.log(buf);
}
buf = "";
state = 0;
}
}
}
})
rl.on('close', function() {
console.log('</resources>')
})

將產生的 XML 檔存放到 res/values 目錄下重新編譯,之後在輸入時,就不用複製 unicode 本身了,除此之外,還可以得到自動完成功能的好處,只要輸入部份文字就會出現選單直接選取。

IconTextView

既然透過指定 typeface 就可以一個 TextView 變成圖示,很直覺的來建立一個專門顯示圖示的 TextView 物件,讓它在建構時直接就設定 typeface 為MDI。

public class IconTextView extends androidx.appcompat.widget.AppCompatTextView {

Context mCtx;

public IconTextView(Context context) {
this(context, null);
}

public IconTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public IconTextView(
Context context,
AttributeSet attrs,
int defStyle) {

super(context, attrs, defStyle);
Typeface tf = Typeface.createFromAsset(
ctx.getAssets(),
"iconfonts/material.ttf");
setTypeface(tf);
}
}

將字型檔放在 asset 目錄下的 iconfonts 目錄下,透過 Typeface物件的靜態函數 createFromAsset 指定好路徑就能讀取進來。

只要在 text 欄位指定對應的 resource id,就能輕鬆的顯示圖示了。

Font Icon to drawable

雖然 IconTextView 大致上已經可以解決我大部分的問題了,還是會遇到一個小狀況,那就是有時候在使用一些 UI library 的時候,它可以讓你自訂圖示,可是參數的型別是 Drawable,也就是需要先有圖片檔。

考慮到一致性,是可以到 MDI 網站上將圖示以圖片的方式下載,不過這樣的過程很繁複冗長,如果需要來回調整可真是太麻煩了。

後來想到,既然 TextView 可以顯示圖片了,何不偷偷在 background 建立一個 TextView,設定好字型、顏色、大小等資訊,然後再從它的 Drawing Cache 裡撈 Bitmap 回來,這樣就可以 run-time 建立 Drawable 了,於是寫了個函數來將字型裡的圖示轉成 Drawable。

如此一來,即使是使用 Drawable 當參數的元件,依然可以使用字串 resource ID 對應的方式來 顯示 MDI 圖示了。

public static BitmapDrawable getDrawableFromFont(
Context ctx,
Int resId,
Typeface tf,
int color,
int textSize,
int size) {

TextView tv = new TextView(ctx);
tv.setTypeface(tf);
tv.setText(resId);
tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize);
tv.setTextColor(color);
tv.setGravity(Gravity.CENTER);
int dp = dpToPx(size, ctx);
tv.measure(
View.MeasureSpec.makeMeasureSpec(
dp,
View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(
dp,
View.MeasureSpec.EXACTLY)
);

tv.layout(0, 0, dp, dp);
tv.invalidate();
tv.setDrawingCacheEnabled(true);
tv.buildDrawingCache();
Bitmap bitmap = tv.getDrawingCache();
BitmapDrawable d = new BitmapDrawable(
ctx.getResources(),
bitmap);
return d;
}

--

--

No responses yet