Android截屏方案实现原理解析
Android截屏的原理:获取具体需要截屏的区域的Bitmap,然后绘制在画布上,保存为图片后进行分享或者其它用途
在截屏功能中,有时需要截取全屏的内容,有时需要截取超过一屏的内容(比如:Listview,Scrollview,RecyclerView)。下面介绍各种场景获取Bitmap的方法
普通截屏的实现
获取当前Window的DrawingCache的方式,即decorView的DrawingCache
/**
*shotthecurrentscreen,withthestatusbutthestatusistrans*
*
*@paramctxcurrentactivity
*/
publicstaticBitmapshotActivity(Activityctx){
Viewview=ctx.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmapbp=Bitmap.createBitmap(view.getDrawingCache(),0,0,view.getMeasuredWidth(),
view.getMeasuredHeight());
view.setDrawingCacheEnabled(false);
view.destroyDrawingCache();
returnbp;
}
获取当前View的DrawingCache
publicstaticBitmapgetViewBp(Viewv){
if(null==v){
returnnull;
}
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());
}
Bitmapb=Bitmap.createBitmap(v.getDrawingCache(),0,0,v.getMeasuredWidth(),v.getMeasuredHeight());
v.setDrawingCacheEnabled(false);
v.destroyDrawingCache();
returnb;
}
开源方案
在滚动视图中,如果当前View并没有在视图中全部绘制出来,我们可以利用View的ScrollTo()和ScrollBy()方法来移动画布,同时获取当前View的可视部分的DrawingCache,最后进行拼接得到其Bitmap,参考:PGSSoft/scrollscreenshot@[Github]。
Scrollview截屏
三个截屏中,ScrollView最简单,因为ScrollView只有一个childView,虽然没有全部显示在界面上,但是已经全部渲染绘制,因此可以直接调用scrollView.draw(canvas)来完成截图
publicstaticBitmapshotScrollView(ScrollViewscrollView){
inth=0;
Bitmapbitmap=null;
for(inti=0;i
Scrollview截屏
而ListView就是会回收与重用Item,并且只会绘制在屏幕上显示的ItemView,根据stackoverflow上大神的建议,采用一个List来存储Item的视图,这种方案依然不够好,当Item足够多的时候,可能会发生oom。
publicstaticBitmapshotListView(ListViewlistview){
ListAdapteradapter=listview.getAdapter();
intitemscount=adapter.getCount();
intallitemsheight=0;
Listbmps=newArrayList();
for(inti=0;i
RecyclerView截屏
我们都知道,在新的Android版本中,已经可以用RecyclerView来代替使用ListView的场景,相比较ListView,RecyclerView对ItemView的缓存支持的更好。可以采用和ListView相同的方案,这里也是在stackoverflow上看到的方案。
publicstaticBitmapshotRecyclerView(RecyclerViewview){
RecyclerView.Adapteradapter=view.getAdapter();
BitmapbigBitmap=null;
if(adapter!=null){
intsize=adapter.getItemCount();
intheight=0;
Paintpaint=newPaint();
intiHeight=0;
finalintmaxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
//Use1/8thoftheavailablememoryforthismemorycache.
finalintcacheSize=maxMemory/8;
LruCachebitmaCache=newLruCache<>(cacheSize);
for(inti=0;i
上面的方法在截取存在异步加载图片的RecyclerView时候会出现加载不出图片的情况,这里再补充一种滚动式截屏的方法
publicstaticvoidscreenShotRecycleView(finalRecyclerViewmRecyclerView,final
RecycleViewRecCallbackcallBack){
if(mRecyclerView==null){
return;
}
BaseListFragment.MyAdapteradapter=(BaseListFragment.MyAdapter)mRecyclerView.getAdapter();
finalPaintpaint=newPaint();
finalintmaxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
//Use1/8thoftheavailablememoryforthismemorycache.
finalintcacheSize=maxMemory/8;
LruCachebitmaCache=newLruCache<>(cacheSize);
finalintoneScreenHeight=mRecyclerView.getMeasuredHeight();
intshotHeight=0;
if(adapter!=null&&adapter.getData().size()>0){
intheaderSize=adapter.getHeaderLayoutCount();
intdataSize=adapter.getData().size();
for(inti=0;i=headerSize)
adapter.startConvert(holder,adapter.getData().get(i-headerSize));
holder.itemView.measure(
View.MeasureSpec.makeMeasureSpec(mRecyclerView.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();
BitmapdrawingCache=holder.itemView.getDrawingCache();
//holder.itemView.destroyDrawingCache();//释放缓存占用的资源
if(drawingCache!=null){
bitmaCache.put(String.valueOf(i),drawingCache);
}
shotHeight+=holder.itemView.getHeight();
if(shotHeight>12000){
//设置截图最大值
if(callBack!=null)
callBack.onRecFinished(null);
return;
}
}
//添加底部高度(加载更多或loading布局高度,此处为固定值:)
finalintfootHight=Util.dip2px(mRecyclerView.getContext(),42);
shotHeight+=footHight;
//返回到顶部
while(mRecyclerView.canScrollVertically(-1)){
mRecyclerView.scrollBy(0,-oneScreenHeight);
}
//绘制截图的背景
finalBitmapbigBitmap=Bitmap.createBitmap(mRecyclerView.getMeasuredWidth(),shotHeight,Bitmap.Config.ARGB_8888);
finalCanvasbigCanvas=newCanvas(bigBitmap);
DrawablelBackground=mRecyclerView.getBackground();
if(lBackgroundinstanceofColorDrawable){
ColorDrawablelColorDrawable=(ColorDrawable)lBackground;
intlColor=lColorDrawable.getColor();
bigCanvas.drawColor(lColor);
}
finalint[]drawOffset={0};
finalCanvascanvas=newCanvas();
if(shotHeight<=oneScreenHeight){
//仅有一页
Bitmapbitmap=Bitmap.createBitmap(mRecyclerView.getWidth(),mRecyclerView.getHeight(),Bitmap.Config.ARGB_8888);
canvas.setBitmap(bitmap);
mRecyclerView.draw(canvas);
if(callBack!=null)
callBack.onRecFinished(bitmap);
}else{
//超过一页
finalintfinalShotHeight=shotHeight;
mRecyclerView.postDelayed(newRunnable(){
@Override
publicvoidrun(){
if((drawOffset[0]+oneScreenHeight0&&leftHeight>0){
Bitmapbitmap=Bitmap.createBitmap(mRecyclerView.getWidth(),mRecyclerView.getHeight(),Bitmap.Config.ARGB_8888);
canvas.setBitmap(bitmap);
mRecyclerView.draw(canvas);
//截图,只要补足的那块图
bitmap=Bitmap.createBitmap(bitmap,0,top,bitmap.getWidth(),leftHeight,null,false);
bigCanvas.drawBitmap(bitmap,0,drawOffset[0],paint);
try{
bitmap.recycle();
}catch(Exceptionex){
ex.printStackTrace();
}
}
if(callBack!=null)
callBack.onRecFinished(bigBitmap);
}
}
},10);
}
}
}
publicinterfaceRecycleViewRecCallback{
voidonRecFinished(Bitmapbitmap);
}
相信有不少小伙伴用BRVH第三方库来做recycleview的适配器的。使用这个库的话再用上面的方法会报角标越界的错误,看了BRVH的源码
publicvoidonBindViewHolder(ViewHolderholder,intpositions){
intviewType=holder.getItemViewType();
switch(viewType){
case0:
this.convert((BaseViewHolder)holder,this.mData.get(holder.getLayoutPosition()-this.getHeaderLayoutCount()));
case273:
case819:
case1365:
break;
case546:
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即可,代码如下
publicclassMyAdapterextendsBaseQuickAdapter{
publicMyAdapter(){
super(getItemLayoutResId(),datas);
}
/**
*用于对外暴露convert方法,构造缓存视图(截屏用)
*@paramviewHolder
*@paramt
*/
publicvoidstartConvert(BaseViewHolderviewHolder,Tt){
convert(viewHolder,t);
}
@Override
protectedvoidconvert(BaseViewHolderviewHolder,Tt){
bindView(viewHolder,t);
}
}
然后将上面所述的获取Bitmap方法修改一下
/**
*截取recyclerview
*/
publicstaticBitmapgetRecyclerViewScreenshot(RecyclerViewview){
BaseListFragment.MyAdapteradapter=(BaseListFragment.MyAdapter)view.getAdapter();
BitmapbigBitmap=null;
if(adapter!=null){
intsize=adapter.getData().size();
intheight=0;
Paintpaint=newPaint();
intiHeight=0;
finalintmaxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
//Use1/8thoftheavailablememoryforthismemorycache.
finalintcacheSize=maxMemory/8;
LruCachebitmaCache=newLruCache<>(cacheSize);
for(inti=0;i
合成Bitmap
比如四张合成一张
/**
*将四张图拼成一张
*
*@parampic1图一
*@parampic2图二
*@parampic3图三
*@parampic4图四
*@returnonly_bitmap
*详情见说明:{@linkcom.bertadata.qxb.util.ScreenShotUtils}
*/
publicstaticBitmapcombineBitmapsIntoOnlyOne(Bitmappic1,Bitmappic2,Bitmappic3,Bitmappic4,Activitycontext){
intw_total=pic2.getWidth();
inth_total=pic1.getHeight()+pic2.getHeight()+pic3.getHeight()+pic4.getHeight();
inth_pic1=pic1.getHeight();
inth_pic4=pic4.getHeight();
inth_pic12=pic1.getHeight()+pic2.getHeight();
//此处为防止OOM需要对高度做限制
if(h_total>HEIGHTLIMIT){
returnnull;
}
Bitmaponly_bitmap=Bitmap.createBitmap(w_total,h_total,Bitmap.Config.ARGB_4444);
Canvascanvas=newCanvas(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);
returnonly_bitmap;
}
图片后期处理
/**
*将传入的Bitmap合理压缩后输出到系统截屏目录下
*命名格式为:Screenshot+时间戳+启信宝报名.jpg
*同时通知系统重新扫描系统文件
*
*@parampic1图一标题栏截图
*@parampic2图二scrollview截图
*@paramcontext用于通知重新扫描文件系统,为提升性能可去掉
*详情见说明:{@linkcom.bertadata.qxb.util.ScreenShotUtils}
*/
publicstaticvoidsavingBitmapIntoFile(finalBitmappic1,finalBitmappic2,finalActivitycontext,finalBitmapAndFileCallBackcallBack){
if(context==null||context.isFinishing()){
return;
}
Threadthread=newThread(newRunnable(){
@Override
publicvoidrun(){
StringfileReturnPath="";
intw=pic1.getWidth();
Bitmapbottom=BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_picture_combine_bottom);
Bitmaptop_banner=BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_picture_combine_top);
Bitmapbitmap_bottom=anyRatioCompressing(bottom,(float)w/bottom.getWidth(),(float)w/bottom.getWidth());
Bitmapbitmap_top=anyRatioCompressing(top_banner,(float)w/bottom.getWidth(),(float)w/bottom.getWidth());
finalBitmaponly_bitmap=combineBitmapsIntoOnlyOne(bitmap_top,pic1,pic2,bitmap_bottom,context);
//获取当前时间
SimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-dd-HH-mm-ss-ms",Locale.getDefault());
Stringdata=sdf.format(newDate());
//获取内存路径
//设置图片路径+命名规范
//声明输出文件
StringstoragePath=Environment.getExternalStorageDirectory().getAbsolutePath();
StringfileTitle="Screenshot_"+data+"_com.bertadata.qxb.biz_info.jpg";
StringfilePath=storagePath+"/DCIM/";
finalStringfileAbsolutePath=filePath+fileTitle;
Filefile=newFile(fileAbsolutePath);
/**
*质压与比压结合
*分级压缩
*输出文件
*/
if(only_bitmap!=null){
try{
//首先,对原图进行一步质量压缩,形成初步文件
FileOutputStreamfos=newFileOutputStream(file);
only_bitmap.compress(Bitmap.CompressFormat.JPEG,50,fos);
//另建一个文件other_file预备输出
Stringother_fileTitle="Screenshot_"+data+"_com.bertadata.qxb.jpg";
Stringother_fileAbsolutePath=filePath+other_fileTitle;
Fileother_file=newFile(other_fileAbsolutePath);
FileOutputStreamother_fos=newFileOutputStream(other_file);
//其次,要判断质压之后的文件大小,按文件大小分级进行处理
longfile_size=file.length()/1024;//sizeoffile(KB)
if(file_size<0||!(file.exists())){
//零级:文件判空
thrownewNullPointerException();
}elseif(file_size>0&&file_size<=256){
//一级:直接输出
deleteFile(other_file);
//通知刷新文件系统,显示最新截取的图文件
fileReturnPath=fileAbsolutePath;
context.sendBroadcast(newIntent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,Uri.parse("file://"+fileAbsolutePath)));
}elseif(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(newIntent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,Uri.parse("file://"+other_fileAbsolutePath)));
}elseif(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(newIntent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,Uri.parse("file://"+other_fileAbsolutePath)));
}elseif(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(newIntent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,Uri.parse("file://"+other_fileAbsolutePath)));
}elseif(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(newIntent(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(Exceptione){
e.printStackTrace();
}
}elsecallBack.onSuccess(null,"");
}
});
thread.start();
}
/**
*可实现任意宽高比例压缩(宽高压比可不同)的压缩方法(主要用于微压)
*
*@parambitmap源图
*@paramwidth_ratio宽压比(float)(0<&&<1)
*@paramheight_ratio高压比(float)(0<&&<1)
*@return目标图片
*
*/
publicstaticBitmapanyRatioCompressing(Bitmapbitmap,floatwidth_ratio,floatheight_ratio){
Matrixmatrix=newMatrix();
matrix.postScale(width_ratio,height_ratio);
returnBitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,false);
}
总结
以上所述是小编给大家介绍的Android截屏方案实现原理解析,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!