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]+oneScreenHeight 0&&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截屏方案实现原理解析,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!