Glide實(shí)現(xiàn)WebView離線圖片的酷炫展示效果

前言

自從交房后,每天除了上班奈惑,大部分時(shí)間都是在地鐵和公交上了吭净。不過有了這些時(shí)間,可以好好看看文章打打基礎(chǔ)肴甸,方便之后換新的環(huán)境寂殉。玩Android收錄了很多值得閱讀的文章,好的文章需要多讀幾次才有所收獲原在。但收錄但文章在手機(jī)上閱讀有一些東西比較影響閱讀體驗(yàn)友扰,比如廣告,比如要點(diǎn)擊取消折疊展開文章晤斩。這兩個(gè)已經(jīng)在Wandroid客戶端做了優(yōu)化焕檬,同時(shí)將文章內(nèi)容改成了深色模式姆坚,總的來說閱讀體驗(yàn)提高了好多澳泵。
當(dāng)在地鐵中到了某些路段,網(wǎng)絡(luò)信號(hào)很差兼呵,網(wǎng)頁經(jīng)常加載不出來兔辅。因此離線閱讀對(duì)我來說變得很重要了。所以在端午期間击喂,我新增了離線閱讀功能维苔,同時(shí)為了能更好的查看文章中的圖片,加入了圖片展示功能懂昂。具體可以看如下效果:

離線閱讀和圖片展示

WebView網(wǎng)頁保存

為了能夠?qū)崿F(xiàn)文章離線閱讀介时,需要將整個(gè)網(wǎng)頁保存下來。我們主要關(guān)注的是html內(nèi)容和相關(guān)的圖片Gif資源》腥幔基于Chromium實(shí)現(xiàn)的WebView本身也會(huì)在網(wǎng)頁加載時(shí)緩存網(wǎng)頁的資源(css/js/圖片等)循衰。為了方便圖片控制展示,這邊選擇通過Glide緩存WebView中的圖片與Gif資源褐澎。

文本保存

通過document.documentElement.outerHTML可以獲得網(wǎng)頁的html內(nèi)容会钝,可以在webview中通過addJavascriptInterface方法傳入用于js層調(diào)用java層的對(duì)象如android。于是我們可以通過如下方式保存網(wǎng)頁內(nèi)容:

 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代碼中不要寫注釋工三,否則會(huì)加載失敗迁酸。WebView中加載js腳本比較難調(diào)試,我們可以在chrome://inspectConsole控制臺(tái)下調(diào)試代碼的正確性俭正。

對(duì)應(yīng)addJavascriptInterface對(duì)象需要有如下方法奸鬓,我們可以將html內(nèi)容保存到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中的圖片,保證點(diǎn)擊縮放展示功能中圖片的流暢性掸读,我們將圖片資源放到Glide中緩存全蝶。這樣webview中的圖片使用Glide加載,點(diǎn)擊圖片展示再用Glide加載時(shí)可以共享緩存資源寺枉。
我們可以通過重寫WebViewClient類的shouldInterceptRequest重定向一些資源請(qǐng)求抑淫。不過一些圖片資源的url并不是嚴(yán)格按照.jpg/png/gif的格式,無法判斷一些url是否是圖片資源姥闪。因此需要通過head請(qǐng)求獲取content-type始苇。同時(shí)還需要將結(jié)果保存起來(用于離線情況,okhttp并不支持head請(qǐng)求的緩存)筐喳。

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方法中就可以重定向圖片類型的請(qǐng)求了。

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[]數(shù)據(jù)避归,還要區(qū)分圖片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,圖片也會(huì)通過它緩存到內(nèi)存和磁盤中梳毙。

圖片展示

為了頂部的效果圖哺窄,需要添加圖片的點(diǎn)擊事件,還要知道圖片所在屏幕中的位置和尺寸(用于轉(zhuǎn)場(chǎng)效果)账锹。

添加點(diǎn)擊事件萌业,獲取圖片位置

在webview加載網(wǎng)頁結(jié)束后,我們給每個(gè)img添加onclick事件奸柬,獲取圖片地址生年,尺寸,位置信息廓奕。一些站點(diǎn)(如微信)圖片是懶加載的抱婉,在離線模式下由于跨域問題最終導(dǎo)致圖片無法加載档叔。因此需要從dataset中取出url重新設(shè)置。還有一些站點(diǎn)(CSDN)本身有點(diǎn)擊展示效果蒸绩,需要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(瀏覽器寬度)呢,在調(diào)試中(見下圖)侵贵,我們發(fā)現(xiàn)通過getBoundingClientRect獲取的尺寸寬度和手機(jī)屏幕的寬度并不是一個(gè)單位届搁。因此需要傳outerWidth用于Android端ImageView實(shí)際尺寸的計(jì)算。

圖片尺寸位置獲取

圖片共享元素轉(zhuǎn)場(chǎng)效果

在頁面加載完成后窍育,我們手動(dòng)注入設(shè)置圖片點(diǎn)擊事件的js代碼卡睦。當(dāng)點(diǎn)擊圖片時(shí),就可以得到圖片url漱抓,尺寸表锻,位置信息。在Android端就可以通過共享元素實(shí)現(xiàn)轉(zhuǎn)場(chǎng)效果了乞娄。再次之前我們需要在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中綁定自定義屬性瞬逊,實(shí)現(xiàn)共享元素轉(zhuǎn)場(chǎng)效果遏考。

@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)
        }
    }
}

項(xiàng)目地址

https://github.com/iamyours/Wandroid

  • 暗黑系列
  • 全網(wǎng)獨(dú)一適配 掘金/簡(jiǎn)書/CSDN/公眾號(hào)/玩Android文章黑夜模式
  • 無廣告理盆,無需點(diǎn)擊展開
  • 圖片顯示,支持縮放科雳,共享元素?zé)o縫轉(zhuǎn)場(chǎng)
  • 支持離線閱讀范删,地鐵上閱讀更方便

下載地址v1.1.0

后續(xù)功能

  • 代碼圖片展示(開發(fā)中蕾域,現(xiàn)支持掘金,簡(jiǎn)書)
  • 文章分類收藏
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末到旦,一起剝皮案震驚了整個(gè)濱河市旨巷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌添忘,老刑警劉巖采呐,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異搁骑,居然都是意外死亡斧吐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門靶病,熙熙樓的掌柜王于貴愁眉苦臉地迎上來会通,“玉大人口予,你說我怎么就攤上這事娄周。” “怎么了沪停?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵煤辨,是天一觀的道長(zhǎng)裳涛。 經(jīng)常有香客問我,道長(zhǎng)众辨,這世上最難降的妖魔是什么端三? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮鹃彻,結(jié)果婚禮上郊闯,老公的妹妹穿的比我還像新娘。我一直安慰自己蛛株,他們只是感情好团赁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谨履,像睡著了一般欢摄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笋粟,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天怀挠,我揣著相機(jī)與錄音,去河邊找鬼害捕。 笑死绿淋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尝盼。 我是一名探鬼主播躬它,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼东涡!你這毒婦竟也來了冯吓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤疮跑,失蹤者是張志新(化名)和其女友劉穎组贺,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祖娘,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡失尖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渐苏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掀潮。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖琼富,靈堂內(nèi)的尸體忽然破棺而出仪吧,到底是詐尸還是另有隱情,我是刑警寧澤鞠眉,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布薯鼠,位于F島的核電站择诈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏出皇。R本人自食惡果不足惜羞芍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望郊艘。 院中可真熱鬧荷科,春花似錦、人聲如沸纱注。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奈附。三九已至全度,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間斥滤,已是汗流浹背将鸵。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留佑颇,地道東北人顶掉。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像挑胸,于是被迫代替她去往敵國(guó)和親痒筒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354