Android截屏與WebView長圖分享經驗總結

最近在做新業務需求的同時,我們在 Android 上遇到了一些之前沒有碰到過的問題,截屏分享、 WebView 生成長圖以及長圖在各個分享渠道分享時圖片模糊甚至分享失敗等問題,在這過程中踩了很多坑,到目前為止絕大部分的問題都還算是有了比較滿意的解決方案。以下就從三個方面來總結一下過程中遇到的挑戰和最后的解決方案。

一、概述

最近在做新業務需求的同時,我們在 Android 上遇到了一些之前沒有碰到過的問題,截屏分享、 WebView 生成長圖以及長圖在各個分享渠道分享時圖片模糊甚至分享失敗等問題,在這過程中踩了很多坑,到目前為止絕大部分的問題都還算是有了比較滿意的解決方案。以下就從三個方面來總結一下過程中遇到的挑戰和最后的解決方案。

二、截圖分享

在 Android 原生系統中是沒有提供截圖的廣播或者監聽事件的,也就是說代碼層面無法獲知用戶的截屏操作,這樣就無法滿足用戶截屏后跳出分享提示的需求。既然無法從根本上解決截屏監聽的問題,那么就要考慮通過其他方式間接實現,目前比較成熟穩定的方案是監聽系統媒體數據庫資源的變化,具體方案原理如下:

Android 系統有一個媒體數據庫,每拍一張照片,或使用系統截屏截取一張圖片,都會把這張圖片的詳細信息加入到這個媒體數據庫,并發出內容改變通知,我們可以利用內容觀察者(ContentObserver)監聽媒體數據庫的變化,當數據庫有變化時,獲取最后插入的一條圖片數據,如果該圖片符合特定的規則,則認為被截屏了。

考慮到手機存儲包括內部存儲器和外部存儲器,為了增強兼容性,最好同時監聽兩種儲存空間的變化,以下是需要 ContentObserver 監聽的資源 URI :

MediaStore.Images.Media.INTERNAL_CONTENT_URI  
MediaStore.Images.Media.EXTERNAL_CONTENT_URI 

讀取外部存儲器資源,需要添加權限:

android.permission.READ_EXTERNAL_STORAGE 

注:在 Android 6.0 及以上版本需要動態申請權限

1. 截屏判斷規則

當 ContentObserver 監聽到媒體數據庫的數據改變, 在有數據改變時獲取最后插入數據庫的一條圖片數據, 如果符合以下規則, 則認為截屏了:

  1. 時間判斷:通常截屏生成后會立馬存入系統多媒體數據庫,也就是說監聽到數據庫變化的時間與截圖生成的時間不會相差太多,這里推薦以10秒作為閾值,當然這個也是經驗值。
  2. 尺寸判斷:截屏顧名思義取得是當前手機屏幕尺寸大小的圖片,所以圖片寬高大于屏幕寬高的肯定都不是截圖產生的。
  3. 路徑判斷:由于各手機廠家存放截圖的文件路徑都不太一樣,國內情況可能會更嚴重,但是通常圖片保存路徑都會包含一些常見的關鍵詞,比如 “screenshot”、 “screencapture” 、 “screencap” 、 “截圖”、 “截屏”等,每次都檢查圖片路徑信息是否包含這些關鍵詞。

關于第3點需要補充說明一下,由于要判斷圖片文件路徑是否包含關鍵字,所以目前僅支持中英文環境,如果需要支持其他語言,需要手動添加一些該語言的關鍵詞,否則有可能獲取不到圖片。

以上3點基本上可以保證截圖的正常監聽,當然在實際測試過程中,還會發現有些機型存在多報的情況,所以還需要做一些去重等工作,關于去重下面還會再提及。

2. 關鍵代碼

原理都了解清楚了,那么接下來就是如何實現的問題了。這里最關鍵是媒體內容觀察者的設置,從數據庫中取出第一條數據并解析圖片信息,然后再檢驗圖片信息是否符合以上3條規則。

為了說清楚如何監聽媒體數據庫改變,先要稍微講一下 ContentObserver 的原理。 ContentObserver ——內容觀察者,目的是觀察(捕捉)特定 Uri 引起的數據庫的變化,繼而做一些相應的處理,它類似于數據庫技術中的觸發器(Trigger),當 ContentObserver 所觀察的 Uri 發生變化時,便會觸發它。當然想要觀察就必須先要注冊, Android 系統提供了 ContentResolver#registerContentObserver 方法用來注冊觀察器。此部分不熟悉的同學可以溫習一下 Android 的 ContentProvider 相關知識。

接下來直接用代碼說明整個注冊和觸發流程,代碼如下:

private void initMediaContentObserver() { 
    // 運行在 UI 線程的 Handler, 用于運行監聽器回調  
    private final Handler mUiHandler = new Handler(Looper.getMainLooper()); 
    // 創建內容觀察者,包括內部存儲和外部存儲 
    mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler); 
    mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler); 
    // 注冊內容觀察者 
    mContext.getContentResolver().registerContentObserver( 
            MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, mInternalObserver); 
    mContext.getContentResolver().registerContentObserver( 
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver); 
} 
/** 
 * 自定義媒體內容觀察者類(觀察媒體數據庫的改變) 
 */ 
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); 
        // 處理媒體數據庫反饋的數據變化 
        handleMediaContentChange(mediaContentUri); 
    } 
} 

有注冊就需要在 Activity 銷毀時取消注冊,所以還需要封裝一個解除注冊的方法供外部調用, Android 系統提供 ContentResolver#unregisterContentObserver 方法來取消注冊,代碼比較簡單,這里就不再展示了。

監聽器設置和注冊完成后,一旦用戶操作了截屏動作,系統就會執行 ContentObserver#onChange 回調方法,在這個方法中我們可以根據 Uri 獲取并解析數據。這里展示一下具體的數據解析過程,上述提到的規則判斷比較簡單,就不再展示了。

private void handleMediaContentChange(Uri contentUri) { 
    Cursor cursor = null; 
        try { 
            // 數據改變時查詢數據庫中最后加入的一條數據 
            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獲取數據庫列索引 
            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);     // 根據路徑獲取圖片寬和高 
                width = size.x; 
                height = size.y; 
            } 
            // 處理獲取到的第一行數據,分別判斷路徑是否包含關鍵詞、時間差以及圖片寬高和屏幕寬高的大小關系 
            handleMediaRowData(data, dateTaken, width, height); 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } finally { 
            if (cursor != null && !cursor.isClosed()) { 
                cursor.close(); 
            } 
        } 
} 

有些手機 ROM 截屏一次會發出多次內容改變的通知,因此需要做去重操作,去重也不復雜,可以用列表緩存最近十幾條圖片地址數據,每次獲取到新的圖片地址,都會先判斷緩存中是否存在相同的圖片地址,如果當前的圖片地址已經存在列表中,則直接過濾掉即可,否則添加到緩存中。如此就可以保證截屏監聽事件既不遺漏也不重復。

以上就是手機截屏的核心原理和關鍵代碼,如果需要分享截屏圖片也很簡單, data 即為圖片的存儲地址,轉換成 Bitmap 即可完成分享。

三、WebView 生成長圖

介紹 web 長圖之前,先來說一下單屏圖片的生成方案,和手機截圖不同的是生成的圖片不會顯示頂部的狀態欄、標題欄以及底部的菜單欄,可以滿足不同的業務需求。

// WebView 生成當前屏幕大小的圖片,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 網頁生成圖片分享出去,相似的例子就是手機端的各種便簽應用,當便簽內容超出一屏時,就需要將所有的內容生成一張長圖對外分享出去。

WebView 和其他 View 一樣,系統都提供了 draw 方法,可以直接將 View 的內容渲染到畫布上,有了畫布我們就可以在上面繪制其他各種各種的內容,比如底部添加 Logo 圖片,畫紅線框等等。關于 WebView 生成長圖網上已經有很多現成的方案和代碼,以下代碼是經測試過的穩定版本,供參考。

// 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 的網頁保持一致 
Paint paint = new Paint(); 
canvas.drawBitmap(longImage, 0, mWebView.getMeasuredHeight(), paint); 
mWebView.draw(canvas); 

Android 為了提高滾動等各方面的繪制速度,可以為每一個 View 建立一個緩存,使用 View#buildDrawingCache 為自己的 View 建立相應的緩存, 這個 cache 就是一個 bitmap 對象。利用這個功能可以對整個屏幕視圖進行截屏并生成 Bitmap ,也可以獲得指定的 View 的 Bitmap 對象。這里由于還要在原有的圖片上繪制 Logo ,所以直接使用了 WebView 的 draw 方法了。

由于我們的 H5 頁面大部分都是運行在微信的 X5 瀏覽器中,所以為了減少前端的適配工作,我們將騰訊的 X5 瀏覽器內核引入了 Android 工程中,代替系統原生的 WebView 內核,關于 X5 內核的引入后續還會有專門的文章介紹,敬請期待。

這里需要說明一下如何在 X5 內核下生成 Web 長圖,上面代碼展示的系統原生 WebView 生成圖片的方案,但是在 X5 環境下上述代碼就失效了,經過踩坑以及查看 X5 內核源代碼,最終我們找到了解決該問題的方法,下面用關鍵代碼來說明一下具體的實現方式。

// 這里的 mWebView 就是 X5 內核的 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 的網頁保持一致 
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 內核生成的長圖清晰度比原生 WebView 要差一些,目前還沒有太好的解決方案。

四、長圖分享

一般我們向各個社交平臺上發送的圖片都比較小,最大也就是手機屏幕大小的圖片,再大的就不多見了。但是也有例外,比如微博的長圖、錘子便簽的長圖等等,如果直接將這些圖片通過微信分享 SDK 或者微博分享 SDK 分享出去,就會發現圖片基本上都是模糊的,但是將圖片發送給 iPhone 手機就可以正常查看,我們只能哀嘆 Android 版微信不給力。

微信 SDK 不給力,但是產品體驗還是不能丟,怎么辦呢?辦法還是有的,我們都知道除了各個社交平臺自己的分享 SDK ,系統提供了原生分享方案,本質上就是社交平臺把目標 Activity 對外暴露了出來,然后第三方 App 就可以根據事先定義好的 Intent 跳轉規則喚起社交平臺,同時完成數據傳輸和展示。

好像問題可以完美解決了,但是還是有坑需要接著踩。在 Android 7.0 及以上的版本系統限制了 Intent 傳輸 file:// 開頭的數據,這也就限制了系統原生分享單圖,怎么辦呢?兩種方案,一種是在 7.0 及以上版本上使用微信等分享 SDK ,接受分享圖片模糊的現狀,另一種是通過反射跳過系統對以 file:// 開頭文件在 Intent 中傳輸的限制,但是這種方式會有風險,畢竟我們不知道未來 Android 會做出什么調整。以下是跳過系統限制的代碼片段,供參考。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 
    try { 
        Method ddfu = StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure"); 
        ddfu.invoke(null); 
    } catch (Exception e) { 
    } 
} 

至此基本上可以滿足任意圖片大小的分享了。此外經過驗證還發現微信分享 Android 版 SDK 對縮略圖和分享圖的大小都有限制,官方給的指導意見是縮略圖小于 32K ,分享圖片小于 10M 即可正常分享,但是試驗下來這兩個值都是理論上限,不要太接近這個上限,如果圖片太大,縮略圖和分享圖都會出現模糊的情況,甚至無法正常分享,當然對于通過系統分享的話就不存在這個限制,圖片也比較清晰。

除了圖片大小有限制,縮略圖的尺寸也是有限制的,這一點官方文檔并沒有給出,試驗結果顯示圖片尺寸小于等于120×120是比較安全的范圍,分享都沒有問題。

五、小結

截屏監聽、 WebView 生成長圖以及長圖分享都是我們團隊之前未曾遇到過的業務需求,在滿足產品業務需求的同時,也踩了很多坑,積累了一些經驗,特此總結。

 

來自:http://mobile.51cto.com/ahot-540923.htm

 

 本文由用戶 ReiCaringto 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!