前言
自从交房后,每天除了上班,大部分时间都是在地铁和公交上了。不过有了这些时间,可以好好看看文章打打基础,方便之后换新的环境。玩Android收录了很多值得阅读的文章,好的文章需要多读几次才有所收获。但收录但文章在手机上阅读有一些东西比较影响阅读体验,比如广告,比如要点击取消折叠展开文章。这两个已经在Wandroid客户端做了优化,同时将文章内容改成了深色模式,总的来说阅读体验提高了好多。
当在地铁中到了某些路段,网络信号很差,网页经常加载不出来。因此离线阅读对我来说变得很重要了。所以在端午期间,我新增了离线阅读功能,同时为了能更好的查看文章中的图片,加入了图片展示功能。具体可以看如下效果:
WebView网页保存
为了能够实现文章离线阅读,需要将整个网页保存下来。我们主要关注的是html内容和相关的图片Gif资源。基于Chromium实现的WebView本身也会在网页加载时缓存网页的资源(css/js/图片等)。为了方便图片控制展示,这边选择通过Glide
缓存WebView中的图片与Gif资源。
文本保存
通过document.documentElement.outerHTML
可以获得网页的html内容,可以在webview中通过addJavascriptInterface
方法传入用于js层调用java层的对象如android
。于是我们可以通过如下方式保存网页内容:
private fun downloadHtml() {
val script = """
javascript:(function(){
var url = document.URL.toString();
var html = document.documentElement.outerHTML;
android.saveHtml(url,html);
})();
""".trimIndent()
webView.loadUrl(script)
}
特别注意的是js代码中不要写注释,否则会加载失败。WebView中加载js脚本比较难调试,我们可以在
chrome://inspect
中Console
控制台下调试代码的正确性。
对应addJavascriptInterface
对象需要有如下方法,我们可以将html内容保存到sd卡下或者/data/data/${application}
目录下
/**
* 离线保存html
*/
@JavascriptInterface
fun saveHtml(url: String, html: String) {
loading.postValue(true)
Constants.IO.execute {
FileUtil.saveHtml(url, html)
msg.postValue("下载成功")
loading.postValue(false)
}
}
图片缓存
为了方便控制webview中的图片,保证点击缩放展示功能中图片的流畅性,我们将图片资源放到Glide中缓存。这样webview中的图片使用Glide加载,点击图片展示再用Glide加载时可以共享缓存资源。
我们可以通过重写WebViewClient
类的shouldInterceptRequest
重定向一些资源请求。不过一些图片资源的url并不是严格按照.jpg/png/gif
的格式,无法判断一些url是否是图片资源。因此需要通过head
请求获取content-type
。同时还需要将结果保存起来(用于离线情况,okhttp并不支持head请求的缓存)。
private val typeDao = AppDataBase.get().urlTypeDao()
fun head(url: String?): String {
val md5 = MD5Utils.stringToMD5(url)
val value = typeDao.getType(md5)
if (value == null) {
val client = OkHttpClient.Builder()
.addNetworkInterceptor(CacheInterceptor())
.build()
val request = Request.Builder()
.url(url)
.head()
.build()
val res = client.newCall(request).execute()
val type = res.header("content-type")
val result = type ?: ""
typeDao.insert(UrlTypeVO(md5, result))
return result
}
return value
}
于是,shouldInterceptRequest
方法中就可以重定向图片类型的请求了。
val head = Wget.head(url)
if (head.startsWith("image")) {
val bytes = GlideUtil.syncLoad(url, head)
if (bytes != null) {
return WebResourceResponse(
head,
"utf-8",
ByteArrayInputStream(bytes)
)
}
}
这里我们需要通过Glide同步获取图片的byte[]
数据,还要区分图片
与gif
。
public class GlideUtil {
public static byte[] syncLoad(String url, String type) {
boolean isGif = type.endsWith("gif");
if (isGif) {
try {
FutureTarget<byte[]> target = Glide.with(App.instance)
.as(byte[].class)
.load(url)
.decode(GifDrawable.class).submit();
return target.get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
FutureTarget<Bitmap> target = Glide.with(App.instance)
.asBitmap().load(url).submit();
try {
Bitmap bitmap = target.get();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
至此webview中图片的加载最终通过的是Glide
,图片也会通过它缓存到内存和磁盘中。
图片展示
为了顶部的效果图,需要添加图片的点击事件,还要知道图片所在屏幕中的位置和尺寸(用于转场效果)。
添加点击事件,获取图片位置
在webview加载网页结束后,我们给每个img
添加onclick
事件,获取图片地址,尺寸,位置信息。一些站点(如微信)图片是懒加载的,在离线模式下由于跨域问题最终导致图片无法加载。因此需要从dataset中取出url重新设置。还有一些站点(CSDN)本身有点击展示效果,需要stopPropagation
阻止事件冒泡屏蔽。
var imgs = document.getElementsByTagName("img");
for(var i=0;i<imgs.length;i++){
var dataset = imgs[i].dataset;
if(dataset && dataset.src && dataset.src!=imgs[i].src){
imgs[i].src = dataset.src;
}
imgs[i].onclick = function(e){
var target = e.target;
var rect = target.getBoundingClientRect();
android.showImage(target.src,rect.x,rect.y,rect.width,rect.height,outerWidth);
e.stopPropagation();
};
}
这里为什么还要在传outerWidth
(浏览器宽度)呢,在调试中(见下图),我们发现通过getBoundingClientRect
获取的尺寸宽度和手机屏幕的宽度并不是一个单位。因此需要传outerWidth
用于Android端ImageView
实际尺寸的计算。
图片共享元素转场效果
在页面加载完成后,我们手动注入设置图片点击事件的js代码。当点击图片时,就可以得到图片url,尺寸,位置信息。在Android端就可以通过共享元素实现转场效果了。再次之前我们需要在WebView所在的布局文件中加入ImageView
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<io.github.iamyours.wandroid.widget.WanWebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never" />
<io.github.iamyours.wandroid.widget.TouchImageView
android:id="@+id/showImage"
android:layout_width="100dp"
android:layout_height="100dp"
android:visibility="invisible"
app:showImage="@{vm.image}" />
</FrameLayout>
通过DataBinding
中绑定自定义属性,实现共享元素转场效果。
@BindingAdapter(value = ["showImage"])
fun bindImage(iv: ImageView, showImage: PositionImage?) {
showImage?.run {
val lp = iv.layoutParams as ViewGroup.MarginLayoutParams
val parentWidth = iv.context.resources.displayMetrics.widthPixels
val scale = parentWidth / clientWidth
lp.width = (width * scale).toInt()
lp.height = (height * scale).toInt()
lp.leftMargin = (x * scale).toInt()
lp.topMargin = (y * scale).toInt()
iv.layoutParams = lp
iv.requestLayout()
iv.displayWithUrl(url, lp.width, lp.height) {
iv.postDelayed({
val activity = iv.getActivity()
activity?.let {
val pair: Pair<View, String> = Pair(iv, "image")
val option =
ActivityOptionsCompat.makeSceneTransitionAnimation(
it,
pair
)
val intent = Intent(it, ImageShowActivity::class.java)
intent.putExtra("url", url)
it.startActivityForResult(intent, 1, option.toBundle())
}
}, 200)
}
}
}
项目地址
https://github.com/iamyours/Wandroid
- 暗黑系列
- 全网独一适配 掘金/简书/CSDN/公众号/玩Android文章黑夜模式
- 无广告,无需点击展开
- 图片显示,支持缩放,共享元素无缝转场
- 支持离线阅读,地铁上阅读更方便
后续功能
- 代码图片展示(开发中,现支持掘金,简书)
- 文章分类收藏