Android截屏與WebView長圖分享經(jīng)驗(yàn)總結(jié)
最近在做新業(yè)務(wù)需求的同時,我們在 Android 上遇到了一些之前沒有碰到過的問題,截屏分享、 WebView 生成長圖以及長圖在各個分享渠道分享時圖片模糊甚至分享失敗等問題,在這過程中踩了很多坑,到目前為止絕大部分的問題都還算是有了比較滿意的解決方案。以下就從三個方面來總結(jié)一下過程中遇到的挑戰(zhàn)和***的解決方案。
一、概述
最近在做新業(yè)務(wù)需求的同時,我們在 Android 上遇到了一些之前沒有碰到過的問題,截屏分享、 WebView 生成長圖以及長圖在各個分享渠道分享時圖片模糊甚至分享失敗等問題,在這過程中踩了很多坑,到目前為止絕大部分的問題都還算是有了比較滿意的解決方案。以下就從三個方面來總結(jié)一下過程中遇到的挑戰(zhàn)和***的解決方案。
二、截圖分享
在 Android 原生系統(tǒng)中是沒有提供截圖的廣播或者監(jiān)聽事件的,也就是說代碼層面無法獲知用戶的截屏操作,這樣就無法滿足用戶截屏后跳出分享提示的需求。既然無法從根本上解決截屏監(jiān)聽的問題,那么就要考慮通過其他方式間接實(shí)現(xiàn),目前比較成熟穩(wěn)定的方案是監(jiān)聽系統(tǒng)媒體數(shù)據(jù)庫資源的變化,具體方案原理如下:
Android 系統(tǒng)有一個媒體數(shù)據(jù)庫,每拍一張照片,或使用系統(tǒng)截屏截取一張圖片,都會把這張圖片的詳細(xì)信息加入到這個媒體數(shù)據(jù)庫,并發(fā)出內(nèi)容改變通知,我們可以利用內(nèi)容觀察者(ContentObserver)監(jiān)聽媒體數(shù)據(jù)庫的變化,當(dāng)數(shù)據(jù)庫有變化時,獲取***插入的一條圖片數(shù)據(jù),如果該圖片符合特定的規(guī)則,則認(rèn)為被截屏了。
考慮到手機(jī)存儲包括內(nèi)部存儲器和外部存儲器,為了增強(qiáng)兼容性,***同時監(jiān)聽兩種儲存空間的變化,以下是需要 ContentObserver 監(jiān)聽的資源 URI :
- MediaStore.Images.Media.INTERNAL_CONTENT_URI
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI
讀取外部存儲器資源,需要添加權(quán)限:
- android.permission.READ_EXTERNAL_STORAGE
注:在 Android 6.0 及以上版本需要動態(tài)申請權(quán)限
1. 截屏判斷規(guī)則
當(dāng) ContentObserver 監(jiān)聽到媒體數(shù)據(jù)庫的數(shù)據(jù)改變, 在有數(shù)據(jù)改變時獲取***插入數(shù)據(jù)庫的一條圖片數(shù)據(jù), 如果符合以下規(guī)則, 則認(rèn)為截屏了:
- 時間判斷:通常截屏生成后會立馬存入系統(tǒng)多媒體數(shù)據(jù)庫,也就是說監(jiān)聽到數(shù)據(jù)庫變化的時間與截圖生成的時間不會相差太多,這里推薦以10秒作為閾值,當(dāng)然這個也是經(jīng)驗(yàn)值。
- 尺寸判斷:截屏顧名思義取得是當(dāng)前手機(jī)屏幕尺寸大小的圖片,所以圖片寬高大于屏幕寬高的肯定都不是截圖產(chǎn)生的。
- 路徑判斷:由于各手機(jī)廠家存放截圖的文件路徑都不太一樣,國內(nèi)情況可能會更嚴(yán)重,但是通常圖片保存路徑都會包含一些常見的關(guān)鍵詞,比如 “screenshot”、 “screencapture” 、 “screencap” 、 “截圖”、 “截屏”等,每次都檢查圖片路徑信息是否包含這些關(guān)鍵詞。
關(guān)于第3點(diǎn)需要補(bǔ)充說明一下,由于要判斷圖片文件路徑是否包含關(guān)鍵字,所以目前僅支持中英文環(huán)境,如果需要支持其他語言,需要手動添加一些該語言的關(guān)鍵詞,否則有可能獲取不到圖片。
以上3點(diǎn)基本上可以保證截圖的正常監(jiān)聽,當(dāng)然在實(shí)際測試過程中,還會發(fā)現(xiàn)有些機(jī)型存在多報(bào)的情況,所以還需要做一些去重等工作,關(guān)于去重下面還會再提及。
2. 關(guān)鍵代碼
原理都了解清楚了,那么接下來就是如何實(shí)現(xiàn)的問題了。這里最關(guān)鍵是媒體內(nèi)容觀察者的設(shè)置,從數(shù)據(jù)庫中取出***條數(shù)據(jù)并解析圖片信息,然后再檢驗(yàn)圖片信息是否符合以上3條規(guī)則。
為了說清楚如何監(jiān)聽媒體數(shù)據(jù)庫改變,先要稍微講一下 ContentObserver 的原理。 ContentObserver ——內(nèi)容觀察者,目的是觀察(捕捉)特定 Uri 引起的數(shù)據(jù)庫的變化,繼而做一些相應(yīng)的處理,它類似于數(shù)據(jù)庫技術(shù)中的觸發(fā)器(Trigger),當(dāng) ContentObserver 所觀察的 Uri 發(fā)生變化時,便會觸發(fā)它。當(dāng)然想要觀察就必須先要注冊, Android 系統(tǒng)提供了 ContentResolver#registerContentObserver 方法用來注冊觀察器。此部分不熟悉的同學(xué)可以溫習(xí)一下 Android 的 ContentProvider 相關(guān)知識。
接下來直接用代碼說明整個注冊和觸發(fā)流程,代碼如下:
- private void initMediaContentObserver() {
- // 運(yùn)行在 UI 線程的 Handler, 用于運(yùn)行監(jiān)聽器回調(diào)
- private final Handler mUiHandler = new Handler(Looper.getMainLooper());
- // 創(chuàng)建內(nèi)容觀察者,包括內(nèi)部存儲和外部存儲
- mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler);
- mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler);
- // 注冊內(nèi)容觀察者
- mContext.getContentResolver().registerContentObserver(
- MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, mInternalObserver);
- mContext.getContentResolver().registerContentObserver(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver);
- }
- /**
- * 自定義媒體內(nèi)容觀察者類(觀察媒體數(shù)據(jù)庫的改變)
- */
- private class MediaContentObserver extends ContentObserver {
- private Uri mediaContentUri; // 需要觀察的Uri
- public MediaContentObserver(Uri contentUri, Handler handler) {
- super(handler);
- mediaContentUri = contentUri;
- }
- @Override
- public void onChange(boolean selfChange) {
- super.onChange(selfChange);
- // 處理媒體數(shù)據(jù)庫反饋的數(shù)據(jù)變化
- handleMediaContentChange(mediaContentUri);
- }
- }
有注冊就需要在 Activity 銷毀時取消注冊,所以還需要封裝一個解除注冊的方法供外部調(diào)用, Android 系統(tǒng)提供 ContentResolver#unregisterContentObserver 方法來取消注冊,代碼比較簡單,這里就不再展示了。
監(jiān)聽器設(shè)置和注冊完成后,一旦用戶操作了截屏動作,系統(tǒng)就會執(zhí)行 ContentObserver#onChange 回調(diào)方法,在這個方法中我們可以根據(jù) Uri 獲取并解析數(shù)據(jù)。這里展示一下具體的數(shù)據(jù)解析過程,上述提到的規(guī)則判斷比較簡單,就不再展示了。
- private void handleMediaContentChange(Uri contentUri) {
- Cursor cursor = null;
- try {
- // 數(shù)據(jù)改變時查詢數(shù)據(jù)庫中***加入的一條數(shù)據(jù)
- cursor = mContext.getContentResolver().query(contentUri,
- Build.VERSION.SDK_INT < 16 ? MEDIA_PROJECTIONS : MEDIA_PROJECTIONS_API_16,
- null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1");
- if (cursor == null) return;
- if (!cursor.moveToFirst()) return;
- // cursor.getColumnIndex獲取數(shù)據(jù)庫列索引
- int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
- String data = cursor.getString(dataIndex); // 圖片存儲地址
- int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
- long dateTaken = cursor.getLong(dateTakenIndex); // 圖片生成時間
- int width = 0;
- int height = 0;
- if (Build.VERSION.SDK_INT >= 16) {
- int widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH);
- int heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT);
- width = cursor.getInt(widthIndex); // 獲取圖片高度
- height = cursor.getInt(heightIndex); // 獲取圖片寬度
- } else {
- Point size = getImageSize(data); // 根據(jù)路徑獲取圖片寬和高
- width = size.x;
- height = size.y;
- }
- // 處理獲取到的***行數(shù)據(jù),分別判斷路徑是否包含關(guān)鍵詞、時間差以及圖片寬高和屏幕寬高的大小關(guān)系
- handleMediaRowData(data, dateTaken, width, height);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (cursor != null && !cursor.isClosed()) {
- cursor.close();
- }
- }
- }
有些手機(jī) ROM 截屏一次會發(fā)出多次內(nèi)容改變的通知,因此需要做去重操作,去重也不復(fù)雜,可以用列表緩存最近十幾條圖片地址數(shù)據(jù),每次獲取到新的圖片地址,都會先判斷緩存中是否存在相同的圖片地址,如果當(dāng)前的圖片地址已經(jīng)存在列表中,則直接過濾掉即可,否則添加到緩存中。如此就可以保證截屏監(jiān)聽事件既不遺漏也不重復(fù)。
以上就是手機(jī)截屏的核心原理和關(guān)鍵代碼,如果需要分享截屏圖片也很簡單, data 即為圖片的存儲地址,轉(zhuǎn)換成 Bitmap 即可完成分享。
二、WebView 生成長圖
介紹 web 長圖之前,先來說一下單屏圖片的生成方案,和手機(jī)截圖不同的是生成的圖片不會顯示頂部的狀態(tài)欄、標(biāo)題欄以及底部的菜單欄,可以滿足不同的業(yè)務(wù)需求。
- // WebView 生成當(dāng)前屏幕大小的圖片,shortImage 就是最終生成的圖片
- Bitmap shortImage = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.RGB_565);
- Canvas canvas = new Canvas(shortImage); // 畫布的寬高和屏幕的寬高保持一致
- Paint paint = new Paint();
- canvas.drawBitmap(shortImage, screenWidth, screenHeight, paint);
- mWebView.draw(canvas);
有的時候我們需要將一個長 Web 網(wǎng)頁生成圖片分享出去,相似的例子就是手機(jī)端的各種便簽應(yīng)用,當(dāng)便簽內(nèi)容超出一屏?xí)r,就需要將所有的內(nèi)容生成一張長圖對外分享出去。
WebView 和其他 View 一樣,系統(tǒng)都提供了 draw 方法,可以直接將 View 的內(nèi)容渲染到畫布上,有了畫布我們就可以在上面繪制其他各種各種的內(nèi)容,比如底部添加 Logo 圖片,畫紅線框等等。關(guān)于 WebView 生成長圖網(wǎng)上已經(jīng)有很多現(xiàn)成的方案和代碼,以下代碼是經(jīng)測試過的穩(wěn)定版本,供參考。
- // WebView 生成長圖,也就是超過一屏的圖片,代碼中的 longImage 就是***生成的長圖
- mWebView.measure(MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- mWebView.layout(0, 0, mWebView.getMeasuredWidth(), mWebView.getMeasuredHeight());
- mWebView.setDrawingCacheEnabled(true);
- mWebView.buildDrawingCache();
- Bitmap longImage = Bitmap.createBitmap(mWebView.getMeasuredWidth(),
- mWebView.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(longImage); // 畫布的寬高和 WebView 的網(wǎng)頁保持一致
- Paint paint = new Paint();
- canvas.drawBitmap(longImage, 0, mWebView.getMeasuredHeight(), paint);
- mWebView.draw(canvas);
Android 為了提高滾動等各方面的繪制速度,可以為每一個 View 建立一個緩存,使用 View#buildDrawingCache 為自己的 View 建立相應(yīng)的緩存, 這個 cache 就是一個 bitmap 對象。利用這個功能可以對整個屏幕視圖進(jìn)行截屏并生成 Bitmap ,也可以獲得指定的 View 的 Bitmap 對象。這里由于還要在原有的圖片上繪制 Logo ,所以直接使用了 WebView 的 draw 方法了。
由于我們的 H5 頁面大部分都是運(yùn)行在微信的 X5 瀏覽器中,所以為了減少前端的適配工作,我們將騰訊的 X5 瀏覽器內(nèi)核引入了 Android 工程中,代替系統(tǒng)原生的 WebView 內(nèi)核,關(guān)于 X5 內(nèi)核的引入后續(xù)還會有專門的文章介紹,敬請期待。
這里需要說明一下如何在 X5 內(nèi)核下生成 Web 長圖,上面代碼展示的系統(tǒng)原生 WebView 生成圖片的方案,但是在 X5 環(huán)境下上述代碼就失效了,經(jīng)過踩坑以及查看 X5 內(nèi)核源代碼,最終我們找到了解決該問題的方法,下面用關(guān)鍵代碼來說明一下具體的實(shí)現(xiàn)方式。
- // 這里的 mWebView 就是 X5 內(nèi)核的 WebView ,代碼中的 longImage 就是***生成的長圖
- mWebView.measure(MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- mWebView.layout(0, 0, mWebView.getMeasuredWidth(), mWebView.getMeasuredHeight());
- mWebView.setDrawingCacheEnabled(true);
- mWebView.buildDrawingCache();
- Bitmap longImage = Bitmap.createBitmap(mWebView.getMeasuredWidth(),
- mWebView.getMeasuredHeight() + endHeight, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(longImage); // 畫布的寬高和 WebView 的網(wǎng)頁保持一致
- Paint paint = new Paint();
- canvas.drawBitmap(longImage, 0, mWebView.getMeasuredHeight(), paint);
- float scale = getResources().getDisplayMetrics().density;
- x5Bitmap = Bitmap.createBitmap(mWebView.getWidth(), mWebView.getHeight(), Bitmap.Config.ARGB_8888);
- Canvas x5Canvas = new Canvas(x5Bitmap);
- x5Canvas.drawColor(ContextCompat.getColor(this, R.color.fragment_default_background));
- mWebView.getX5WebViewExtension().snapshotWholePage(x5Canvas, false, false); // 少了這行代碼就無法正常生成長圖
- Matrix matrix = new Matrix();
- matrix.setScale(scale, scale);
- longCanvas.drawBitmap(x5Bitmap, matrix, paint);
注:X5 內(nèi)核生成的長圖清晰度比原生 WebView 要差一些,目前還沒有太好的解決方案。
三、長圖分享
一般我們向各個社交平臺上發(fā)送的圖片都比較小,***也就是手機(jī)屏幕大小的圖片,再大的就不多見了。但是也有例外,比如微博的長圖、錘子便簽的長圖等等,如果直接將這些圖片通過微信分享 SDK 或者微博分享 SDK 分享出去,就會發(fā)現(xiàn)圖片基本上都是模糊的,但是將圖片發(fā)送給 iPhone 手機(jī)就可以正常查看,我們只能哀嘆 Android 版微信不給力。
微信 SDK 不給力,但是產(chǎn)品體驗(yàn)還是不能丟,怎么辦呢?辦法還是有的,我們都知道除了各個社交平臺自己的分享 SDK ,系統(tǒng)提供了原生分享方案,本質(zhì)上就是社交平臺把目標(biāo) Activity 對外暴露了出來,然后第三方 App 就可以根據(jù)事先定義好的 Intent 跳轉(zhuǎn)規(guī)則喚起社交平臺,同時完成數(shù)據(jù)傳輸和展示。
好像問題可以***解決了,但是還是有坑需要接著踩。在 Android 7.0 及以上的版本系統(tǒng)限制了 Intent 傳輸 file:// 開頭的數(shù)據(jù),這也就限制了系統(tǒng)原生分享單圖,怎么辦呢?兩種方案,一種是在 7.0 及以上版本上使用微信等分享 SDK ,接受分享圖片模糊的現(xiàn)狀,另一種是通過反射跳過系統(tǒng)對以 file:// 開頭文件在 Intent 中傳輸?shù)南拗?,但是這種方式會有風(fēng)險,畢竟我們不知道未來 Android 會做出什么調(diào)整。以下是跳過系統(tǒng)限制的代碼片段,供參考。
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- try {
- Method ddfu = StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure");
- ddfu.invoke(null);
- } catch (Exception e) {
- }
- }
至此基本上可以滿足任意圖片大小的分享了。此外經(jīng)過驗(yàn)證還發(fā)現(xiàn)微信分享 Android 版 SDK 對縮略圖和分享圖的大小都有限制,官方給的指導(dǎo)意見是縮略圖小于 32K ,分享圖片小于 10M 即可正常分享,但是試驗(yàn)下來這兩個值都是理論上限,不要太接近這個上限,如果圖片太大,縮略圖和分享圖都會出現(xiàn)模糊的情況,甚至無法正常分享,當(dāng)然對于通過系統(tǒng)分享的話就不存在這個限制,圖片也比較清晰。
除了圖片大小有限制,縮略圖的尺寸也是有限制的,這一點(diǎn)官方文檔并沒有給出,試驗(yàn)結(jié)果顯示圖片尺寸小于等于120×120是比較安全的范圍,分享都沒有問題。
四、小結(jié)
截屏監(jiān)聽、 WebView 生成長圖以及長圖分享都是我們團(tuán)隊(duì)之前未曾遇到過的業(yè)務(wù)需求,在滿足產(chǎn)品業(yè)務(wù)需求的同時,也踩了很多坑,積累了一些經(jīng)驗(yàn),特此總結(jié)。