WebView適配文章黑夜模式

爲了可以讓簡書,掘金,CSDN,公衆號的文章展現成黑夜模式,須要webview作相關適配。原理其實也比較簡單,只要加載頁面時替換相關的css樣式作替換。實際實現效果每一個站點各有不一樣,下面就介紹下每一個站點是如何作實現的。javascript

項目地址

github.com/iamyours/Wa…css

簡書

reader-night-mode

簡書網站是有黑夜模式的,因此實現起來相對簡單。可是默認用webview加載簡書文章時,它顯示的是日間模式效果。打開chrome調試器,而後再簡書上切換黑夜模式,咱們能夠看到使用黑夜模式時,body會有一個reader-night-mode的class樣式加進去。 html

1

猜想簡書的黑夜模式和這個class樣式有關,那咱們能夠經過前端

WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
複製代碼

調試webview,在chrome瀏覽器上輸入chrome://inspect,而後就能夠調試web頁面了。咱們打開一篇簡書文章,經過調試器咱們將body的樣式替換成reader-night-mode,就會發現當前文章已經變成黑夜模式的了。 vue

2

展開全文,去導航,去廣告

爲了使閱讀體驗更好,咱們在打開文章時直接展開全文,同時去掉導航還有廣告等和文章內容無關的元素,咱們先經過調試器作測試。 java

3
4

正則替換css

經過剛剛的調試,發現這些效果對應的css樣式是在當前html頁面的head標籤下,並非經過css文件形式。所以先經過OkHttp請求文章地址生成html字符串,而後經過正則替換相關css。 先建立一個Wget工具類,用於將網頁轉成字符串,這裏注意請求頭固定成移動設備。android

object Wget {
    fun get(url: String): String {
        val client = OkHttpClient.Builder()
            .build()
        val request = Request.Builder()
            .url(url)
            .header(
                "user-agent",
                "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3887.7 Mobile Safari/537.36"
            )
            .build()
        val response = client.newCall(request).execute()
        return response.body()?.string() ?: ""
    }
}
複製代碼

而後建立一個JianShuWebClient,適配簡書css。那些寫在head標籤下的樣式,經過觀察發現統一寫在了<style data-vue-ssr-id>下面,咱們只需經過正則表達式找到它,而後replace替換咱們放在assets下的css(拷貝自原style下的css,作了相關修改),而後將body的樣式替換成reader-night-modecss3

class JianShuWebClient:WebViewClient(){

    override fun shouldInterceptRequest(view: WebView?, url: String?)
            : WebResourceResponse? {
        val urlStr = url ?: ""
        if (urlStr.startsWith("https://www.jianshu.com/p/")) {
            val response = Wget.get(url ?: "")
            val res = darkBody(replaceCss(response, view!!.context))
            val input = ByteArrayInputStream(res.toByteArray())
            return WebResourceResponse("text/html", "utf-8", input)
        }
        return super.shouldInterceptRequest(view, url)
    }

    private val rex = "(<style data-vue-ssr-id=[\\s\\S]*?>)([\\s\\S]*]?)(<\\/style>)"
    private val bodyRex = "<body class=\"([\\ss\\S]*?)\""
    private fun darkBody(res: String): String {
        val pattern = Pattern.compile(bodyRex)
        val m = pattern.matcher(res)
        return if (m.find()) {
            val s = "<body class=\"reader-night-mode normal-size\""
            res.replace(bodyRex.toRegex(), s)
        } else res
    }

    private fun replaceCss(res: String, context: Context): String {
        val pattern = Pattern.compile(rex)
        val m = pattern.matcher(res)
        return if (m.find()) {
            val css = StringUtil.getString(context.assets.open("jianshu/jianshu.css"))
            val sb = StringBuilder()
            sb.append(m.group(1))
            sb.append(css)
            sb.append(m.group(3))
            val res = res.replace(rex.toRegex(), sb.toString())
            Log.e("test", "$res")
            res
        } else {
            res
        }
    }
}
複製代碼

效果

5

掘金

主css文件替換

掘金網站是沒有黑夜模式的(Android上有),所以適配起來相比簡書麻煩一些。與簡書不一樣的是,掘金文章的樣式是經過css文件外部引入的,因此就不須要OkHttp請求轉換字符串了。咱們直接找到對應的文件在shouldInterceptRequest方法中替換掉git

override fun shouldInterceptRequest(view: WebView?, url: String?)
            : WebResourceResponse? {
        Log.i("掘金", "url:$url")
        val urlStr = url ?: ""
        if (urlStr.startsWith("https://b-gold-cdn.xitu.io/v3/static/css/0")
            && urlStr.endsWith(".css")
        ) {
            val stream = view!!.context.assets.open("juejin/css/juejin.css")
            return WebResourceResponse("text/css", "utf-8", stream)
        }

        return super.shouldInterceptRequest(view, url)
    }
複製代碼

經過插件能夠看到掘金前端是經過vue編寫的,編譯的css會自帶[data-v-xxx]的信息,每次更新時的xxx號碼會更高,咱們須要將[data]信息去除。參照簡書黑夜模式的樣式,咱們在juejin.css加入黑夜模式的樣式。github

.article-area{padding:0 8px;background:#3f3f3f;color:#969696;}//背景,字體顏色
blockquote{background:#555;border-left:3px solid #222;margin:0px;padding:5px 16px;}//引用
code{color:#c7254e;border-radius:4px;background-color:#282828;padding:2px 4px;font-size:12px;}//代碼
.hljs {
	display: block;
	padding: 5px;
	color: #abb2bf;
	background: #282c34;
	border-radius:4px;
	font-size:12px;
}

 .hljs-comment, .hljs-quote {//代碼關鍵字顏色
	color: #5c6370;
	font-style: italic
}
...還有不少,具體見項目
複製代碼

具體要注意的是背景顏色,文字顏色,代碼背景,顏色,引用,表格等等。

圖片問題,頭像問題

掘金文章的圖片是經過懶加載,使用替換的css,發現裏面的圖片顯示不了了。因此在頁面加載完成時注入圖片顯示腳本具體以下

val script = """ javascript:(function(){ var arr = document.getElementsByClassName("lazyload"); for(var i=0;i<arr.length;i++){ var img = arr[i]; var src = img.getAttribute("data-src"); img.src = src; } })(); """
webview.loadUrl(script)
複製代碼

頭像則經過接口獲取用戶數據而後,經過javascript修改。

private var head = ""
    private var username = ""

    private fun loadUser() {
        val client = OkHttpClient.Builder().build()
        val req = Request.Builder().url(detailApi).build()
        val call = client.newCall(req)
        call.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
            }

            override fun onResponse(call: Call, response: Response) {
                val res = response.body()?.string() ?: "{}"
                val obj =
                    Gson().fromJson<JsonObject>(res, JsonObject::class.java)
                obj?.getAsJsonObject("d")
                    ?.getAsJsonObject("user")?.run {
                        head = get("avatarLarge").asString
                        username = get("username").asString
                    }

            }
        })
    }


    private fun getDetailApi(postId: String): String {//頭像沒有加載,手動調用
        return "https://post-storage-api-ms.juejin" +
                ".im/v1/getDetailData?src=web&type=entry&postId=$postId"
    }

    fun loadUserScript() {
            val script = """ javascript:(function(){ document.getElementsByClassName("author-info-block")[0].children[0].children[0].style.backgroundImage = "url('$head')"; document.getElementsByClassName("username")[0].innerHTML="$username"; })(); """.trimIndent()
            webView.loadUrl(script)
        }

複製代碼

效果

6

一樣的CSDN的適配也掘金差很少,也是經過替換css文件完成的,這裏便再也不講述具體適配。

微信公衆號

微信公衆號文章的樣式同簡書同樣也是放在當前html內部。正則表達式有所不一樣

val rex = "(<style>)([\\S ]*)(</style>)"
複製代碼

具體的意思是匹配style標籤,而且內容包含字符,或者空格(換行不算)。

important強制替換

有些微信公衆號裏的文字標籤(如p標籤)自己自帶了style樣式,很差經過正則替換。然而css3有一個!important能夠提升優先級,強制設置相關標籤的屬性(即使它身設置了style樣式)。

7

固然important也不能濫用,不然一些你並不想改的樣式(如代碼)也都修改了,因此css選擇器要準確匹配纔可設置!important

相關文章
相關標籤/搜索