WebView适配文章黑夜模式

作者: 星星y | 来源:发表于2019-09-22 00:42 被阅读0次

    为了能够让简书,掘金,CSDN,公众号的文章展示成黑夜模式,需要webview做相关适配。原理其实也比较简单,只要加载页面时替换相关的css样式做替换。实际实现效果每个站点各有不同,下面就介绍下每个站点是如何做实现的。

    项目地址

    https://github.com/iamyours/Wandroid

    简书

    reader-night-mode

    简书网站是有黑夜模式的,所以实现起来相对简单。但是默认用webview加载简书文章时,它显示的是日间模式效果。打开chrome调试器,然后再简书上切换黑夜模式,我们可以看到使用黑夜模式时,body会有一个reader-night-mode的class样式加进去。

    1

    猜测简书的黑夜模式和这个class样式有关,那我们可以通过

    WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
    

    调试webview,在chrome浏览器上输入chrome://inspect,然后就可以调试web页面了。我们打开一篇简书文章,通过调试器我们将body的样式替换成reader-night-mode,就会发现当前文章已经变成黑夜模式的了。

    2

    展开全文,去导航,去广告

    为了使阅读体验更好,我们在打开文章时直接展开全文,同时去掉导航还有广告等和文章内容无关的元素,我们先通过调试器做测试。


    3
    4

    正则替换css

    通过刚刚的调试,发现这些效果对应的css样式是在当前html页面的head标签下,并不是通过css文件形式。因此先通过OkHttp请求文章地址生成html字符串,然后通过正则替换相关css。
    先创建一个Wget工具类,用于将网页转成字符串,这里注意请求头固定成移动设备。

    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-mode

    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方法中替换掉

    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加入黑夜模式的样式。

    .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

    相关文章

      网友评论

        本文标题:WebView适配文章黑夜模式

        本文链接:https://www.haomeiwen.com/subject/oolyuctx.html