去年參加鐵人賽時,已經寫了一篇關於字型調整的文章:電子書閱讀器上的瀏覽器 [Day08] 調整網頁字型。當時介紹了怎麼調整字型粗細,大小,以及如何套用雲端字型。但是,其實我的終極目標是跟電子書閱讀 App 一樣:在設備上安裝更多的字型,然後在 App 中可以任意選擇自己的字型檔。
雖然很久以前我就有試著解決這問題,無奈網上的解決方案都是繞著使用編譯到 apk 中 assets 目錄下的固定字型。這方式無法套用到使用者自行放到手機上的字型檔案;我也不想因此在 2.5 MB 大小的 APP 中,放入一個動輒 10 到 30 MB 的字型檔案,這樣有點太本末倒置了。
在不斷地探索之後,終於讓我試出了一套可行的方式。雖然還不是很完美的方案,但已經有八九十分了吧。希望以後有機會再把它變得更完美。
簡單來說:要讓使用者先選擇一下字型檔案的位置;在 WebView
中注入 CSS style,自定義一個字型,把它的 src 指定到一個特別的 uri;當 WebView
在讀取這個 CSS font style 時,會經過 WebViewClient
的 shouldInterceptRequest()
,這時要把字型檔的 binary stream 餵進去。下面就是詳細的步驟內容。
這步驟不難,只是因為要讓使用者選擇字型的話,還是要把這部分的 UI 邏輯先做好。我寫了一個 openFontFilePicker
的函式,叫起系統的 file picker。
當 Activity
或 Fragment
收到系統回傳的 intent 時,我會把它記錄到 SharedPreferences
中,並且利用第 71, 72 行,取得之後 APP 繼續能夠讀取這個檔案的權限。
塞在 SharedPreferences 的字型資料,寫了一個簡單的 class 儲存檔名和收到的 content uri。
注入的方式,網路上倒是有不少文件,而且其實都寫得大同小異。我定義了一段要注入的 script 如下:新增一個 font-face
,它的 font-family
為 customfont
,來源是隨便訂的 url(‘mycustomfont’),然後把所有 body 的字型全換成我的 customfont。補充說明一下,在參考加入 Google Web fonts 時(https://fonts.googleapis.com/css2?family=Noto+Serif+TC:wght@400&display=swap),看到 Google 字型設定設定中都會加入 font-display: swap;
後來發現,加入後畫面在更換字型時就不會再閃爍。更多的說明可以參見 Controlling Font Performance with font-display。
注入 CSS 的方式之前我也有包成一個函式了:
利用 injectCSS
,將 customFontCSS 注入後,接著要處理 intercept Web Request:在 WebViewClient
中覆寫 shouldInterceptRequest()
。主要處理落在下圖的第 5行到 17 行:如果來的 request url 是事前定義好的 mycustomfont 的話,這就是 Web 要來找字型資訊的時機,10, 11 行透過 context.contentResolver
建立檔案串流,再生成一個 WebResourceResponse
回傳。
基本上這樣子就可以正確地看到手機上的字型檔案有被載入,用來顯示網頁的內容。
步驟三可以看到,每次網頁來要求字型資訊時,我都是建立新的 inputstream,回傳一個新的 WebResourceResponse
。這邊我有嘗試著 re-use inputstream 或是 WebResourceResponse
,但都還沒有成功。所以,也就只能先停在這兒。至少這已經是個可以運作的方案了。
另外一點是,很多網頁的字型都會在不同的 element 中另外設定。這種情況下,直接改 body 的字型會沒有作用,這也造成大部分網頁可以;但少部分的網頁還是會維持原來的字型。這問題稍微可以透過把網頁轉成 Reader mode 來解決,但畢竟不是一個很漂亮的作法。也是希望未來會有更完美的處理方式。
大部分的程式碼都在這個 commit 了,雖然後來還有些小修改。