Glide实现WebView离线图片的酷炫展示效果

前言

自从交房后,每天除了上班,大部分时间都是在地铁和公交上了。不过有了这些时间,可以好好看看文章打打基础,方便之后换新的环境。玩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://inspectConsole控制台下调试代码的正确性。

对应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文章黑夜模式
  • 无广告,无需点击展开
  • 图片显示,支持缩放,共享元素无缝转场
  • 支持离线阅读,地铁上阅读更方便

下载地址v1.1.0

后续功能

  • 代码图片展示(开发中,现支持掘金,简书)
  • 文章分类收藏
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342