file

現在公司開發手機 App 案子的時候,我都傾向於用 WebView 來做;這樣做有幾個好處:

  1. 排版容易:說真的,網頁 CSS 的排版發展真的遠比無論是 Android 還是 iOS 原生的排版機制要先進太多了,尤其再加上例如 Bootstrap 之類的 CSS 框架,排起來又快又有比較好的 RWD 效果。
  2. 更新方便:改變設計的時候只需要自己更新站台就好,不用走一次 App 的改版上架、使用者也不用更新 App。當然,我猜應該是有一些原生的排版框架有支援動態更新版面檔案,但是首先我不熟那類作法,再來我也懷疑那樣的更新有辦法做到連路由和畫面互動邏輯都變更的地步;相對地用 WebView 要直接變更這兩點當然不是問題。
  3. 互動好做:可以使用例如 Vue.jsBlazor 之類的反應式框架,在互動方面也遠比原生要好做。
  4. 機制完備:可以借用瀏覽器核心已經很完備的機制如資源快取、安全連線、cookie 等等,不用再另外自行處理。

當然 WebView 也是有缺點的,最主要的缺點大抵就是它的效能終究是比原生要差,因此如果想要做很炫的元件視覺效果,它並不是很好的選擇(不過因為我公司做的主要都是電子商務相關的 App,對這方面的需求不大)。其次這樣的 App 若要做到可以離線使用,架構上也必須使用 PWA 的作法,會有一些技術上的門檻;再來它要充分模擬出原生的使用者體驗、在套件的選擇以及 WebView 的底層設置上也是需要稍微下一點功夫的。

其中一個讓我花了一些力氣研究出來的效果就是讓 Android 的 WebView 也能夠使用自訂的系統字型。一些品牌的 Android 手機(例如三星等等)支援一種叫 Flipfont 的機制可以替換掉整個系統的預設字型,但是這個設定並不會直接反應在瀏覽器或者 WebView 之上。結果就是系統換了好看的字型,但是瀏覽器仍舊是普通的黑體,而且任何用 WebView 來做的 App 也是,一看就很突兀。後來我研究出讓 WebView 也能呈現出系統字型的辦法如下。

首先,當系統有支援 Flipfont 的時候,Typeface 類別會多出一個非標準的靜態方法可以去取得字型的路徑,但比較麻煩的是因為這是非標準的方法,即使同樣是三星手機也有不同的實作名稱;目前我自己實驗出來的有兩種:getFontPathFlipFont()semGetFontPathOfCurrentFontStyle()。應用這個原理,我們寫下如下的 FontUtil 類別(採用 Kotlin):

import android.content.Context
import android.graphics.Typeface
import java.io.File
import java.lang.reflect.Method

object FontUtil {

    private fun getMethod(name: String): Method? {
        return try {
            Typeface::class.java.getMethod(name, Context::class.java, Int::class.javaPrimitiveType)
        } catch (e: NoSuchMethodException) {
            null
        }
    }

    private fun getFile(pathname: String): File? {
        val file = File(pathname)
        if (file.exists()) return file
        return null
    }

    // 利用 Reflection 的方法抓出 FlipFont 的當前設定
    fun getFlipFont(context: Context): File? {
        val m: Method = getMethod("getFontPathFlipFont")
                ?: getMethod("semGetFontPathOfCurrentFontStyle")
                ?: return null

        val path = m.invoke(null, context, 1)?.toString()
        if (path != "default") {
            return getFile("$path/DroidSansFallback.ttf") ?: getFile("$path/DroidSans.ttf")
        }
        return null
    }
}

其次,在我們的 Activity 中,我們做類似下面這樣的設置:

private val webView by lazy { findViewById<WebView>(...) }
private val flipFont: File? by lazy { FontUtil.getFlipFont(this) }

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    webView.webViewClient = AppWebViewClient()
    ...
}

private inner class AppWebViewClient : WebViewClient() {
    override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
        val url = request.url.toString()
        val font = flipFontResponse(url)
        return font ?: super.shouldInterceptRequest(view, request)
    }
}

 private fun flipFontResponse(url: String): WebResourceResponse? {
     if (url.endsWith("flipfont.ttf") && flipFont != null) {
         try {
             val stream: InputStream = FileInputStream(flipFont!!)
             return WebResourceResponse("application/x-font-ttf", "UTF-8", stream)
         } catch (e: FileNotFoundException) {
            e.printStackTrace()
         }
     }
     return null
 }

這個 AppWebViewClient 的作用是當網頁呼叫「flipfont.ttf」這個檔案的時候就把請求攔截掉,把回應替換成稍早取得的檔案的 stream 去給網頁使用。而最後,我們只要在網頁上使用如下的 CSS:

@font-face {
    font-family: Flipfont;
    src: url(flipfont.ttf);
}

body {
    font-family: Flipfont;
}

就可以成功地使用到 WebView 傳回的字型檔了。

上面展示的程式碼是最基本的概念,當然實際上還有其它優化空間,例如反覆呼叫字型的時候直接傳回 304 代碼讓 WebView 使用快取而非重新讀取檔案、或是在網頁裡下一個判斷確定當前環境是否為 Flipfont(例如可以利用 JavaScript interface 跟原生程式碼確認這件事)等等,這些細節就留給讀者了。


分享此頁至:
最後修改日期: 2021/07/03

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。