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

相关文章
相关标签/搜索