Android截圖方案
Android截圖
Android截圖的原理:獲取具體需要截圖的區域的Bitmap,然後繪製在畫布上,儲存為圖片後進行分享或者其它用途
在截圖功能中,有時需要擷取全屏的內容,有時需要擷取超過一屏的內容(比如:Listview,Scrollview,RecyclerView)。下面介紹各種場景獲取Bitmap的方法
普通截圖的實現
獲取當前Window的DrawingCache的方式,即decorView的DrawingCache
/**
* shot the current screen ,with the status but the status is trans *
*
* @param ctx current activity
*/
public static Bitmap shotActivity(Activity ctx) {
View view = ctx.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bp = Bitmap.createBitmap(view.getDrawingCache(), 0, 0, view.getMeasuredWidth(),
view.getMeasuredHeight());
view.setDrawingCacheEnabled(false);
view.destroyDrawingCache();
return bp;
}
獲取當前View的DrawingCache
public static Bitmap getViewBp(View v) {
if (null == v) {
return null;
}
v.setDrawingCacheEnabled(true);
v.buildDrawingCache();
if (Build.VERSION.SDK_INT >= 11) {
v.measure(MeasureSpec.makeMeasureSpec(v.getWidth(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
v.getHeight(), MeasureSpec.EXACTLY));
v.layout((int ) v.getX(), (int) v.getY(),
(int) v.getX() + v.getMeasuredWidth(),
(int) v.getY() + v.getMeasuredHeight());
} else {
v.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
}
Bitmap b = Bitmap.createBitmap(v.getDrawingCache(), 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
v.setDrawingCacheEnabled(false);
v.destroyDrawingCache();
return b;
}
開源方案
在滾動檢視中,如果當前View並沒有在檢視中全部繪製出來,我們可以利用View的ScrollTo()和ScrollBy()方法來移動畫布,同時獲取當前View的可視部分的DrawingCache,最後進行拼接得到其Bitmap,參考:PGSSoft/[email protected][Github]。
Scrollview截圖
三個截圖中,ScrollView最簡單,因為ScrollView只有一個childView,雖然沒有全部顯示在介面上,但是已經全部渲染繪製,因此可以直接 呼叫scrollView.draw(canvas)
來完成截圖
public static Bitmap shotScrollView(ScrollView scrollView) {
int h = 0;
Bitmap bitmap = null;
for (int i = 0; i < scrollView.getChildCount(); i++) {
h += scrollView.getChildAt(i).getHeight();
scrollView.getChildAt(i).setBackgroundColor(Color.parseColor("#ffffff"));
}
bitmap = Bitmap.createBitmap(scrollView.getWidth(), h, Bitmap.Config.RGB_565);
final Canvas canvas = new Canvas(bitmap);
scrollView.draw(canvas);
return bitmap;
}
listview截圖
而ListView就是會回收與重用Item,並且只會繪製在螢幕上顯示的ItemView,根據stackoverflow上大神的建議,採用一個List來儲存Item的檢視,這種方案依然不夠好,當Item足夠多的時候,可能會發生oom。
public static Bitmap shotListView(ListView listview) {
ListAdapter adapter = listview.getAdapter();
int itemscount = adapter.getCount();
int allitemsheight = 0;
List<Bitmap> bmps = new ArrayList<Bitmap>();
for (int i = 0; i < itemscount; i++) {
View childView = adapter.getView(i, null, listview);
childView.measure(
View.MeasureSpec.makeMeasureSpec(listview.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
childView.setDrawingCacheEnabled(true);
childView.buildDrawingCache();
bmps.add(childView.getDrawingCache());
allitemsheight += childView.getMeasuredHeight();
}
Bitmap bigbitmap =
Bitmap.createBitmap(listview.getMeasuredWidth(), allitemsheight, Bitmap.Config.ARGB_8888);
Canvas bigcanvas = new Canvas(bigbitmap);
Paint paint = new Paint();
int iHeight = 0;
for (int i = 0; i < bmps.size(); i++) {
Bitmap bmp = bmps.get(i);
bigcanvas.drawBitmap(bmp, 0, iHeight, paint);
iHeight += bmp.getHeight();
bmp.recycle();
bmp = null;
}
return bigbitmap;
}
RecyclerView截圖
我們都知道,在新的Android版本中,已經可以用RecyclerView來代替使用ListView的場景,相比較ListView,RecyclerView對Item View的快取支援的更好。可以採用和ListView相同的方案,這裡也是在stackoverflow上看到的方案。
public static Bitmap shotRecyclerView(RecyclerView view) {
RecyclerView.Adapter adapter = view.getAdapter();
Bitmap bigBitmap = null;
if (adapter != null) {
int size = adapter.getItemCount();
int height = 0;
Paint paint = new Paint();
int iHeight = 0;
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
for (int i = 0; i < size; i++) {
RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i));
adapter.onBindViewHolder(holder, i);
holder.itemView.measure(
View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(),
holder.itemView.getMeasuredHeight());
holder.itemView.setDrawingCacheEnabled(true);
holder.itemView.buildDrawingCache();
Bitmap drawingCache = holder.itemView.getDrawingCache();
if (drawingCache != null) {
bitmaCache.put(String.valueOf(i), drawingCache);
}
height += holder.itemView.getMeasuredHeight();
}
bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
Canvas bigCanvas = new Canvas(bigBitmap);
Drawable lBackground = view.getBackground();
if (lBackground instanceof ColorDrawable) {
ColorDrawable lColorDrawable = (ColorDrawable) lBackground;
int lColor = lColorDrawable.getColor();
bigCanvas.drawColor(lColor);
}
for (int i = 0; i < size; i++) {
Bitmap bitmap = bitmaCache.get(String.valueOf(i));
bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
iHeight += bitmap.getHeight();
bitmap.recycle();
}
}
return bigBitmap;
}
相信有不少小夥伴用BRVH第三方庫來做recycleview的介面卡的。使用這個庫的話再用上面的方法會報角標越界的錯誤,看了BRVH的原始碼
public void onBindViewHolder(ViewHolder holder, int positions) {
int viewType = holder.getItemViewType();
switch(viewType) {
case 0:
this.convert((BaseViewHolder)holder, this.mData.get(holder.getLayoutPosition() - this.getHeaderLayoutCount()));
case 273:
case 819:
case 1365:
break;
case 546:
this.addLoadMore(holder);
break;
default:
this.convert((BaseViewHolder)holder, this.mData.get(holder.getLayoutPosition() - this.getHeaderLayoutCount()));
this.onBindDefViewHolder((BaseViewHolder)holder, this.mData.get(holder.getLayoutPosition() - this.getHeaderLayoutCount()));
}
}
在呼叫adapter.onBindViewHolder
時,因為裡面的position
引數未使用,裡面用的計算holder.getLayoutPosition()
- this.getHeaderLayoutCount()
的值一直是-1導致角標越界報錯。
本人理解,RecyclerView的截圖原理是,首先構造每個item的ViewHolder,然後呼叫具體設定資料到每個item的方法,此時cache中就存有item的內容,此時繪製就能獲取到完整的內容。採用v7包中的onBindViewHolder
方法即可,或者是BRVH的convert
方法,可以看到BRVH中沒有暴露出這個方法,而且唯一暴露出的onBindViewHolder
還會報角標越界錯誤,此時我們就需要在BRVH的基礎上暴露出convert
即可,程式碼如下
public class MyAdapter extends BaseQuickAdapter<T> {
public MyAdapter() {
super(getItemLayoutResId(), datas);
}
/**
* 用於對外暴露convert方法,構造快取檢視(截圖用)
* @param viewHolder
* @param t
*/
public void startConvert(BaseViewHolder viewHolder, T t){
convert(viewHolder,t);
}
@Override
protected void convert(BaseViewHolder viewHolder, T t) {
bindView(viewHolder, t);
}
}
然後將上面所述的獲取Bitmap方法修改一下
/**
* 擷取recycler view
*/
public static Bitmap getRecyclerViewScreenshot(RecyclerView view) {
BaseListFragment.MyAdapter adapter = (BaseListFragment.MyAdapter) view.getAdapter();
Bitmap bigBitmap = null;
if (adapter != null) {
int size = adapter.getData().size();
int height = 0;
Paint paint = new Paint();
int iHeight = 0;
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
for (int i = 0; i < size; i++) {
BaseViewHolder holder = (BaseViewHolder) adapter.createViewHolder(view, adapter.getItemViewType(i));
//此處需要呼叫convert方法,否則繪製出來的都是空的item
adapter.startConvert(holder, adapter.getData().get(i));
holder.itemView.measure(
View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(),
holder.itemView.getMeasuredHeight());
holder.itemView.setDrawingCacheEnabled(true);
holder.itemView.buildDrawingCache();
Bitmap drawingCache = holder.itemView.getDrawingCache();
if (drawingCache != null) {
bitmaCache.put(String.valueOf(i), drawingCache);
}
height += holder.itemView.getMeasuredHeight();
}
bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
Canvas bigCanvas = new Canvas(bigBitmap);
Drawable lBackground = view.getBackground();
if (lBackground instanceof ColorDrawable) {
ColorDrawable lColorDrawable = (ColorDrawable) lBackground;
int lColor = lColorDrawable.getColor();
bigCanvas.drawColor(lColor);
}
for (int i = 0; i < size; i++) {
Bitmap bitmap = bitmaCache.get(String.valueOf(i));
bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
iHeight += bitmap.getHeight();
bitmap.recycle();
}
}
return bigBitmap;
}
合成Bitmap
比如四張合成一張
/**
* 將四張圖拼成一張
*
* @param pic1 圖一
* @param pic2 圖二
* @param pic3 圖三
* @param pic4 圖四
* @return only_bitmap
* 詳情見說明:{@link com.bertadata.qxb.util.ScreenShotUtils}
*/
public static Bitmap combineBitmapsIntoOnlyOne(Bitmap pic1, Bitmap pic2, Bitmap pic3, Bitmap pic4, Activity context) {
int w_total = pic2.getWidth();
int h_total = pic1.getHeight() + pic2.getHeight() + pic3.getHeight() + pic4.getHeight();
int h_pic1 = pic1.getHeight();
int h_pic4 = pic4.getHeight();
int h_pic12 = pic1.getHeight() + pic2.getHeight();
//此處為防止OOM需要對高度做限制
if (h_total > HEIGHTLIMIT) {
return null;
}
Bitmap only_bitmap = Bitmap.createBitmap(w_total, h_total, Bitmap.Config.ARGB_4444);
Canvas canvas = new Canvas(only_bitmap);
canvas.drawColor(ContextCompat.getColor(context, R.color.color_content_bg));
canvas.drawBitmap(pic1, 0, 0, null);
canvas.drawBitmap(pic2, 0, h_pic1, null);
canvas.drawBitmap(pic3, 0, h_pic12, null);
canvas.drawBitmap(pic4, 0, h_total - h_pic4, null);
return only_bitmap;
}
圖片後期處理
/**
* 將傳入的Bitmap合理壓縮後輸出到系統截圖目錄下
* 命名格式為:Screenshot+時間戳+啟信寶報名.jpg
* 同時通知系統重新掃描系統檔案
*
* @param pic1 圖一 標題欄截圖
* @param pic2 圖二 scrollview截圖
* @param context 用於通知重新掃描檔案系統,為提升效能可去掉
* 詳情見說明:{@link com.bertadata.qxb.util.ScreenShotUtils}
*/
public static void savingBitmapIntoFile(final Bitmap pic1, final Bitmap pic2, final Activity context, final BitmapAndFileCallBack callBack) {
if (context == null || context.isFinishing()) {
return;
}
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
String fileReturnPath = "";
int w = pic1.getWidth();
Bitmap bottom = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_picture_combine_bottom);
Bitmap top_banner = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_picture_combine_top);
Bitmap bitmap_bottom = anyRatioCompressing(bottom, (float) w / bottom.getWidth(), (float) w / bottom.getWidth());
Bitmap bitmap_top = anyRatioCompressing(top_banner, (float) w / bottom.getWidth(), (float) w / bottom.getWidth());
final Bitmap only_bitmap = combineBitmapsIntoOnlyOne(bitmap_top, pic1, pic2, bitmap_bottom, context);
// 獲取當前時間
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-ms", Locale.getDefault());
String data = sdf.format(new Date());
// 獲取記憶體路徑
// 設定圖片路徑+命名規範
// 宣告輸出檔案
String storagePath = Environment.getExternalStorageDirectory().getAbsolutePath();
String fileTitle = "Screenshot_" + data + "_com.bertadata.qxb.biz_info.jpg";
String filePath = storagePath + "/DCIM/";
final String fileAbsolutePath = filePath + fileTitle;
File file = new File(fileAbsolutePath);
/**
* 質壓與比壓結合
* 分級壓縮
* 輸出檔案
*/
if (only_bitmap != null) {
try {
// 首先,對原圖進行一步質量壓縮,形成初步檔案
FileOutputStream fos = new FileOutputStream(file);
only_bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos);
// 另建一個檔案other_file預備輸出
String other_fileTitle = "Screenshot_" + data + "_com.bertadata.qxb.jpg";
String other_fileAbsolutePath = filePath + other_fileTitle;
File other_file = new File(other_fileAbsolutePath);
FileOutputStream other_fos = new FileOutputStream(other_file);
// 其次,要判斷質壓之後的檔案大小,按檔案大小分級進行處理
long file_size = file.length() / 1024; // size of file(KB)
if (file_size < 0 || !(file.exists())) {
// 零級: 檔案判空
throw new NullPointerException();
} else if (file_size > 0 && file_size <= 256) {
// 一級: 直接輸出
deleteFile(other_file);
// 通知重新整理檔案系統,顯示最新擷取的圖檔案
fileReturnPath = fileAbsolutePath;
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + fileAbsolutePath)));
} else if (file_size > 256 && file_size <= 768) {
// 二級: 簡單壓縮:壓縮為原比例的3/4,質壓為50%
anyRatioCompressing(only_bitmap, (float) 3 / 4, (float) 3 / 4).compress(Bitmap.CompressFormat.JPEG, 40, other_fos);
deleteFile(file);
// 通知重新整理檔案系統,顯示最新擷取的圖檔案
fileReturnPath = other_fileAbsolutePath;
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + other_fileAbsolutePath)));
} else if (file_size > 768 && file_size <= 1280) {
// 三級: 中度壓縮:壓縮為原比例的1/2,質壓為40%
anyRatioCompressing(only_bitmap, (float) 1 / 2, (float) 1 / 2).compress(Bitmap.CompressFormat.JPEG, 40, other_fos);
deleteFile(file);
// 通知重新整理檔案系統,顯示最新擷取的圖檔案
fileReturnPath = other_fileAbsolutePath;
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + other_fileAbsolutePath)));
} else if (file_size > 1280 && file_size <= 2048) {
// 四級: 大幅壓縮:壓縮為原比例的1/3,質壓為40%
anyRatioCompressing(only_bitmap, (float) 1 / 3, (float) 1 / 3).compress(Bitmap.CompressFormat.JPEG, 40, other_fos);
deleteFile(file);
// 通知重新整理檔案系統,顯示最新擷取的圖檔案
fileReturnPath = other_fileAbsolutePath;
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + other_fileAbsolutePath)));
} else if (file_size > 2048) {
// 五級: 中度壓縮:壓縮為原比例的1/2,質壓為40%
anyRatioCompressing(only_bitmap, (float) 1 / 2, (float) 1 / 2).compress(Bitmap.CompressFormat.JPEG, 40, other_fos);
deleteFile(file);
// 通知重新整理檔案系統,顯示最新擷取的圖檔案
fileReturnPath = other_fileAbsolutePath;
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + other_fileAbsolutePath)));
}
// 登出fos;
fos.flush();
other_fos.flush();
other_fos.close();
fos.close();
//callback用於回傳儲存成功的路徑以及Bitmap
callBack.onSuccess(only_bitmap, fileReturnPath);
} catch (Exception e) {
e.printStackTrace();
}
} else callBack.onSuccess(null, "");
}
});
thread.start();
}
/**
* 可實現任意寬高比例壓縮(寬高壓比可不同)的壓縮方法(主要用於微壓)
*
* @param bitmap 源圖
* @param width_ratio 寬壓比(float)(0<&&<1)
* @param height_ratio 高壓比(float)(0<&&<1)
* @return 目標圖片
* <p>
*/
public static Bitmap anyRatioCompressing(Bitmap bitmap, float width_ratio, float height_ratio) {
Matrix matrix = new Matrix();
matrix.postScale(width_ratio, height_ratio);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
}