为了能够让简书,掘金,CSDN,公众号的文章展示成黑夜模式,需要webview做相关适配。原理其实也比较简单,只要加载页面时替换相关的css样式做替换。实际实现效果每个站点各有不同,下面就介绍下每个站点是如何做实现的。
项目地址
https://github.com/iamyours/Wandroid
简书
reader-night-mode
简书网站是有黑夜模式的,所以实现起来相对简单。但是默认用webview加载简书文章时,它显示的是日间模式效果。打开chrome调试器,然后再简书上切换黑夜模式,我们可以看到使用黑夜模式时,body
会有一个reader-night-mode
的class样式加进去。
猜测简书的黑夜模式和这个class样式有关,那我们可以通过
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
调试webview,在chrome浏览器上输入chrome://inspect
,然后就可以调试web页面了。我们打开一篇简书文章,通过调试器我们将body
的样式替换成reader-night-mode
,就会发现当前文章已经变成黑夜模式的了。
展开全文,去导航,去广告
为了使阅读体验更好,我们在打开文章时直接展开全文,同时去掉导航还有广告等和文章内容无关的元素,我们先通过调试器做测试。
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样式)。
当然important也不能滥用,否则一些你并不想改的样式(如代码)也都修改了,所以css选择器要准确匹配才可设置!important
。
网友评论