最近在作新業務需求的同時,咱們在 Android 上遇到了一些以前沒有碰到過的問題,截屏分享、 WebView 生成長圖以及長圖在各個分享渠道分享時圖片模糊甚至分享失敗等問題,在這過程當中踩了不少坑,到目前爲止絕大部分的問題都還算是有了比較滿意的解決方案。如下就從三個方面來總結一下過程當中遇到的挑戰和最後的解決方案。前端
最近在作新業務需求的同時,咱們在 Android 上遇到了一些以前沒有碰到過的問題,截屏分享、 WebView 生成長圖以及長圖在各個分享渠道分享時圖片模糊甚至分享失敗等問題,在這過程當中踩了不少坑,到目前爲止絕大部分的問題都還算是有了比較滿意的解決方案。如下就從三個方面來總結一下過程當中遇到的挑戰和最後的解決方案。android
在 Android 原生系統中是沒有提供截圖的廣播或者監聽事件的,也就是說代碼層面沒法獲知用戶的截屏操做,這樣就沒法知足用戶截屏後跳出分享提示的需求。既然沒法從根本上解決截屏監聽的問題,那麼就要考慮經過其餘方式間接實現,目前比較成熟穩定的方案是監聽系統媒體數據庫資源的變化,具體方案原理以下:web
Android 系統有一個媒體數據庫,每拍一張照片,或使用系統截屏截取一張圖片,都會把這張圖片的詳細信息加入到這個媒體數據庫,併發出內容改變通知,咱們能夠利用內容觀察者(ContentObserver)監聽媒體數據庫的變化,當數據庫有變化時,獲取最後插入的一條圖片數據,若是該圖片符合特定的規則,則認爲被截屏了。面試
考慮到手機存儲包括內部存儲器和外部存儲器,爲了加強兼容性,最好同時監聽兩種儲存空間的變化,如下是須要 ContentObserver 監聽的資源 URI :數據庫
MediaStore.Images.Media.INTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
複製代碼
讀取外部存儲器資源,須要添加權限:canvas
android.permission.READ_EXTERNAL_STORAGE
複製代碼
注:在 Android 6.0 及以上版本須要動態申請權限瀏覽器
當 ContentObserver 監聽到媒體數據庫的數據改變, 在有數據改變時獲取最後插入數據庫的一條圖片數據, 若是符合如下規則, 則認爲截屏了:緩存
時間判斷:一般截屏生成後會立馬存入系統多媒體數據庫,也就是說監聽到數據庫變化的時間與截圖生成的時間不會相差太多,這裏推薦以10秒做爲閾值,固然這個也是經驗值。 尺寸判斷:截屏顧名思義取得是當前手機屏幕尺寸大小的圖片,因此圖片寬高大於屏幕寬高的確定都不是截圖產生的。 路徑判斷:因爲各手機廠家存放截圖的文件路徑都不太同樣,國內狀況可能會更嚴重,可是一般圖片保存路徑都會包含一些常見的關鍵詞,好比 「screenshot」、 「screencapture」 、 「screencap」 、 「截圖」、 「截屏」等,每次都檢查圖片路徑信息是否包含這些關鍵詞。 關於第3點須要補充說明一下,因爲要判斷圖片文件路徑是否包含關鍵字,因此目前僅支持中英文環境,若是須要支持其餘語言,須要手動添加一些該語言的關鍵詞,不然有可能獲取不到圖片。 以上3點基本上能夠保證截圖的正常監聽,固然在實際測試過程當中,還會發現有些機型存在多報的狀況,因此還須要作一些去重等工做,關於去重下面還會再說起。安全
原理都瞭解清楚了,那麼接下來就是如何實現的問題了。這裏最關鍵是媒體內容觀察者的設置,從數據庫中取出第一條數據並解析圖片信息,而後再檢驗圖片信息是否符合以上3條規則。bash
爲了說清楚如何監聽媒體數據庫改變,先要稍微講一下 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 便可完成分享。
介紹 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 就是 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 生成長圖以及長圖分享都是咱們團隊以前不曾遇到過的業務需求,在知足產品業務需求的同時,也踩了不少坑,積累了一些經驗,特此總結。
微信公衆號:終端研發部