webview v html 兼容,WebView适配文章黑夜模式

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

项目地址

简书

reader-night-mode

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

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

WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)

复制代码

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

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

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

5f6e402ffdd37d6d331eebbb0b7edde9.gif

正则替换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标签下的样式,通过观察发现统一写在了

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 = "(

private val bodyRex = "

private fun darkBody(res: String): String {

val pattern = Pattern.compile(bodyRex)

val m = pattern.matcher(res)

return if (m.find()) {

val s = "

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

}

}

}

复制代码

效果

16d54bd444bf68ef?imageslim

掘金

主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

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(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)

}

复制代码

效果

16d54bd448acae5a?imageslim

同样的CSDN的适配也掘金差不多,也是通过替换css文件完成的,这里便不再讲述具体适配。

微信公众号

微信公众号文章的样式同简书一样也是放在当前html内部。正则表达式有所不同

val rex = "()"

复制代码

具体的意思是匹配style标签,并且内容包含字符,或者空格(换行不算)。

important强制替换

有些微信公众号里的文字标签(如p标签)本身自带了style样式,不好通过正则替换。然而css3有一个!important可以提高优先级,强制设置相关标签的属性(即便它身设置了style样式)。0c01e9fa91e9f2d000452468e5322339.gif

当然important也不能滥用,否则一些你并不想改的样式(如代码)也都修改了,所以css选择器要准确匹配才可设置!important。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值