最近工作特忙,好久沒靜下心總結一些開發中的心得,后面會陸續寫一些文章總結一下最近遇到的問題和一些收獲吧~
閑話少說,今天想跟大家分享的是,在android中,如何后臺將一個view繪制成圖片,并簡單梳理下其中遇到的坑。很多app都有這么一個功能,當用戶完成了app的某個任務時,產品希望用戶點擊分享的時候,能動態繪制出一張圖片,讓用戶的分享的內容更加生動化。舉個例子,比如扇貝單詞的打卡,點擊分享到新浪微博的時候,app會動態在后臺生成一張圖片,用戶確認分享就會將這張圖片分享出去。首先確認一下分享的圖片上包含的元素吧:
- ui提供的圖片素材
- 用戶的信息,比如昵稱,頭像等
- 本次任務完成的數據,比如跑了多少km啊,背了多少單詞啊,復雜點的話還會包括一張網絡圖片,需要加載完畢后再生成指定的圖片
比如說向下圖這樣(這算個廣告吧)
首先可以確認的是,直接在View上布局不是一件難事,需要在代碼中操作的信息如前面提到的用戶信息啊,本次任務的數據啊,和兩張需要異步加載的圖片(軌跡圖和頭像)。
首先這不是一個簡單的截屏,有些app會將分享的圖片先展示給用戶,然后當前頁面“截屏”,生成一張圖片,然后調取第三方的圖片分享,總結來說要么是通過View.getDrawingCache()方法拿到當前View的緩存,要么是直接調用Bitmap.createBitmap()生成Bitmap。我們簡單分析一下這兩種做法:
方法1 View.getDrawingCache() 只適用于分享的View已經完整展示在用戶的屏幕上,超出屏幕范圍內的內容是不在生成的Bitmap內的。因為android手機的屏幕尺寸差異太大,通常我們需要生成的圖片不會很短,所以很難保證這點,同時如果當前展示的View和最終生成的圖片有一些差異的話,比如某個按鈕不顯示,某個文字換個內容等,就沒辦法用這種辦法了。
第二種其實也是我們最終采用的方式,不過沒那么簡單,先來看這樣一種做法,在實踐中證明存在挺多問題。不過確實有人在采用,還是說一下吧
假設我當前是在A頁面,我要分享出去的B圖片和A頁面只需要隱藏分享按鈕,這種方法的做法是:
vShare.setVisibility(INVISIBLE);
Bitmap b = Bitmap.createBitmap( v.getLayoutParams().width, v.getLayoutParams().height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
v.draw(c);
return b;
說一下問題在哪,首先生成Bitmap的操作應該是后臺異步操作,當前app應該有一個阻斷態,我們會發現,用戶會觀察到分享按鈕消失了,然后生成圖片后又再次出現,一個按鈕也許還ok,如果界面上的差異很大,這種方式給用戶的體驗就很不好。其次,也是最重要的,通常我們View的布局的寬高都是類似于macth_parent,wrap_content, 分享出去的圖片尺寸無法控制(完全取決于手機的屏幕尺寸),圖片中的一些元素的寬度(例如異步加載的網絡圖片)經過試驗發現是異常的,原因我暫時還不清楚,大家可以和我探討一下。
像我這個項目中需要生成的圖片,和分享頁面可以說差別非常大,那么我們該如何處理呢?
首先單獨寫一個布局,寬高全都是固定值。我設置的寬高單位是dp,也就是說我生成的圖片的實際寬高取決于用戶手機的屏幕密度,好處在于低配手機通常性能是首要考慮目標,尺寸過大很容易導致OOM,這些低配手機的屏幕密度一般都不高,而同時高配手機上,生成的分享圖片如果不夠清晰,給用戶的體驗就很不好(想像一下高清屏幕上的顆粒圖吧)。因為涉及到數據的展示,我這邊采取自定義View的方式,假設名稱叫ShareView,它需要對外暴露這樣幾個方法:
- 根據用戶是否登陸,繪制不同的樣式
- 接收相關數據,填充指定View
- 提供一個生成分享圖片鏈接Uri的方法,并在適當的時機,回調外部的生成圖片成功或失敗的回調方法。
思路確定,這里只提供最核心的代碼:
/**
* 創建分享的圖片文件
*/
public String createShareFile() {
Bitmap bitmap = createBitmap();
//將生成的Bitmap插入到手機的圖片庫當中,獲取到圖片路徑
String filePath = MediaStore.Images.Media.insertImage(getContext().getContentResolver(), bitmap, null, null);
//及時回收Bitmap對象,防止OOM
if (!bitmap.isRecycled()) {
bitmap.recycle();
}
//轉uri之前必須判空,防止保存圖片失敗
if (TextUtils.isEmpty(filePath)) {
return "";
}
return getRealPathFromURI(getContext(), Uri.parse(filePath));
}
/**
* 創建分享Bitmap
*/
private Bitmap createBitmap() {
//自定義ViewGroup,一定要手動調用測量,布局的方法
measure(getLayoutParams().width, getLayoutParams().height);
layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
//如果圖片對透明度無要求,可以設置為RGB_565
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
draw(canvas);
return bitmap;
}
private static String getRealPathFromURI(Context context, Uri contentUri) {
Cursor cursor = null;
try {
String[] proj = {MediaStore.Images.Media.DATA};
cursor = context.getContentResolver().query(contentUri, proj, null, null, null);
if (cursor == null) {
return "";
}
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
return cursor.getString(column_index);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
核心代碼交代完了,說一下一些小點吧:
- 網絡圖片需要加載完成后再回調生成圖片成功的方法,例如在Glide的RequestListener等。
- 從職責單一的角度,我們需要定義一個圖片分享管理器的類,類似于ImageShareManager,諸如處理子線程創建分享圖片,管理生成的圖片(登陸/未登錄),具體如何布局代碼,我想大家都有自己的想法~
- 由于內存考慮和第三方分享圖片時的限制,生成的圖片大小需要在實踐中自己把握,像我項目中現在分享出去的圖片寬高為360dp*892dp, 1080p的手機生成的圖片大概150k,基本上都能成功分享。只遇到2k屏幕的手機,朋友圈分享圖片失敗(微信聊天,qq,qq空間,新浪微博都可以),于是針對2k屏幕,判斷下屏幕密度,如果大于3.0的,我會采用寬高縮小的布局文件。我在項目中嘗試過直接縮放Bitmap,結果發現圖片質量模糊的非常厲害,UI無法接受,應該是強制縮放的效果本身就很差,建議還是對這部分手機單獨處理吧。
- 分享到微信聊天的圖片,需要設置縮略圖,否則對方聊天界面不打開大圖是看不到東西的,另外,網上所謂的32k的限制我沒遇到,就算是也肯定指縮略圖,原圖不超過200k應該還是可以的。(吐槽一下當時幾個同事都信誓旦旦說圖片不能超過32k,害得我折騰了好久,又是縮小布局,又是縮放Bitmap,最后發現原圖根本沒那個限制,轉過頭來問那幾個同事,語氣又不那么確定了...總之不能輕信很多沒經過驗證的說法)
謝謝大家