最近在处理新的业务需求时,在Android上遇到了一些以前没有遇到过的问题。分享时出现图片模糊甚至分享失败等问题。在这个过程中,踩过很多坑。到目前为止,大部分问题都得到了比较满意的解决。下面从三个方面总结过程中遇到的挑战和最佳解决方案。一、概述最近在处理新的业务需求的时候,遇到了一些Android上没有遇到过的问题,比如截图分享,WebView生成长图,各种长图分享时长图模糊甚至无法分享分享渠道,过程中踩了很多坑,目前为止大部分问题都得到了比较满意的解决。下面从三个方面总结过程中遇到的挑战和最佳解决方案。2、截图分享在Android原生系统中,没有截图广播和监听事件,也就是说代码层面无法获知用户的截图操作,所以无法满足用户截图后跳出分享提示的需求截图。既然不能从根本上解决截屏监控的问题,就要考虑其他的方法间接实现。目前比较成熟稳定的方案是监控系统媒体数据库资源的变化。解决方案的具体原理如下:Android系统有一个媒体数据库,每一张A照片,或者系统截图截取的一张图片,都会将这张图片的详细信息添加到媒体数据库中,并发送内容变化通知。我们可以使用内容观察器(ContentObserver)来监测媒体数据库的变化。当数据库发生变化时,获取***插入的一张图片数据,如果图片符合特定规则,则认为是截图。考虑到手机存储包括内部存储和外部存储,为了增强兼容性,最好同时监控两个存储空间的变化。下面是ContentObserver需要监听的资源URI:MediaStore.Images.Media.INTERNAL_CONTENT_URIMediaStore.Images.Media.EXTERNAL_CONTENT_URI读取外部存储资源需要添加权限:android.permission.READ_EXTERNAL_STORAGE注意:在Android6.0及以下以上,需要动态申请权限1.截图判断规则ContentObserver监听媒体数据库中的数据变化时,有数据变化时获取***插入数据库的一张图片数据,如果满足以下规则,则认为是截图:时间判断:通常截图生成后会立即存入系统多媒体数据库,即检测到数据库变化的时间不会与截图生成的时间相差太多,建议使用10秒作为阈值,当然这也是一个经验值。尺寸判断:顾名思义,截图就是当前手机屏幕尺寸的图片,所以宽高大于屏幕宽高的图片肯定不是截图生成的。路径判断:由于各手机厂商存储的截图文件路径不同,国内情况可能更严重,但通常图片存储路径会包含一些常用关键字,如“screenshot”、“screencapture”、“screencap","Screenshot","screenshot"等,每次检查图片路径信息是否包含这些关键字。关于第三点,我需要补充说明。由于需要判断图片文件路径是否包含关键词,目前只支持中文和英文环境。如果需要支持其他语言,需要手动添加该语言的一些关键字,否则可能获取不到。图片。以上3点基本可以保证截图的正常监控。当然在实际测试过程中,有些模型也会发现多报,所以还是需要做一些去重等工作。我将在下面提到重复数据删除。.2.了解了关键的代码原理之后,接下来就是如何实现了。这里最重要的是媒体内容观察者的设置,从数据库中取出第一条数据分析图像信息,然后检查图像信息是否符合以上三个规则。为了弄清楚如何监听媒体数据库的变化,我们先说说ContentObserver的原理。ContentObserver——内容观察者,目的是观察(捕获)特定Uri引起的数据库变化,然后做一些相应的处理,它类似于数据库技术中的触发器(Trigger),当ContentObserver观察到的Uri变化,它会被触发。当然,如果你想观摩,就必须先注册。Android系统提供了ContentResolver#registerContentObserver方法来注册观察者。对这部分不熟悉的同学可以复习一下Android的ContentProvider相关知识。下面直接用代码来说明整个注册和触发流程,代码如下:privatevoidinitMediaContentObserver(){//运行在UI线程上的Handler,用于运行监听回调;//创建内容观察器那些,包括内部存储和外部存储.getContentResolver()。registerContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI,false,mInternalObserver);mContext.getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,false,mExternalObserver);}/***自定义媒体内容观察者类(观察媒体数据库变化)*/privateclassMediaContentObserverextendsContentObserver{privateUrimediaContentUri;//UripublicMediaContentObserver(UricontentUri,Handlerhandler){super(handler);mediaContentUri=contentUri;}@OverridepublicvoidonmediaChange(booleanself/Change){handling)(se/super.数据库反馈数据变化handleMediaContentChange(mediaContentUri);}}如果有注册,需要在Activity销毁的时候注销,所以需要封装一个注销的方法供外部调用。Android系统提供了ContentResolver#unregisterContentObserver方法来注销。代码比较简单,这里不再赘述说明监听器设置注册后,一旦用户截屏,系统会执行ContentObserver#onChange回调方法,在回调方法中我们可以获取并解析数据根据乌里。具体的数据分析过程如下所示。上面说到的规则的判断比较简单,就不再展示了。privatevoidhandleMediaContentChange(UricontentUri){Cursorcursor=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+"desclimit1");if(cursor==null)return;if(!cursor.moveToFirst())return;//cursor.getColumnIndex获取数据库列索引intdataIndex=cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);Stringdata=cursor.getString(dataIndex);//图片存储地址;//图片生成时间intwidth=0;intheight=0;if(Build.VERSION.SDK_INT>=16){intwidthIndex=cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH);intheightIndex=cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT);width=cursor.getInt(widthIndex);//获取图片高度的高度=cursor.getInt(heightIndex);//获取图像的宽度}else{Pointsize=getImageSize(data);//根据路径获取图片宽高width=size.x;height=size.y;}//对获取到的第一行数据进行处理,判断路径是否包含关键字、时间差、图片宽度,height和screen宽高的大小关系handleMediaRowData(data,dateTaken,width,height);}catch(Exceptione){e.printStackTrace();}finally{if(cursor!=null&&!cursor.isClosed()){cursor.close();}}}有些手机ROM会一次发送多个内容变化通知,所以需要去重,去重并不复杂。可以使用list缓存最近十几张图片地址数据,每次获取新的图片地址都会先判断缓存中是否存在相同的图片地址。如果当前图片地址已经存在于列表中,则直接过滤掉,否则将添加到缓存中,这样截图监控事件就不会漏掉,也不会重复。以上就是手机截图的核心原理和关键代码。如果您需要分享屏幕截图,那非常简单。data是图片的存储地址,可以转换成Bitmap完成分享。2、WebView生成长图在介绍网页长图之前,先说说单屏图片的生成方案。与手机截图不同的是,生成的图片不会在顶部显示状态栏,在底部显示标题栏和菜单栏,可以满足不同的业务需求。//WebView生成当前屏幕尺寸的图片,shortImage为最终图片BitmapshortImage=Bitmap.createBitmap(screenWidth,screenHeight,Bitmap.Config.RGB_565);Canvascanvas=newCanvas(shortImage);//宽高画布和屏幕的宽度保持高度一致Paintpaint=newPaint();canvas.drawBitmap(shortImage,screenWidth,screenHeight,paint);mWebView.draw(canvas);有时候我们需要分享一个网页生成的长图,类似的例子就是移动端的各种便利贴应用,当便利贴的内容超过一屏时,需要将所有内容生成一张长图并分享它在外部。WebView与其他View一样。系统提供了draw方法,可以直接将View的内容渲染到canvas上。有了canvas,我们可以在上面绘制其他各种内容,比如在底部添加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();BitmaplongImage=Bitmap.createBitmap(mWebView.getMeasuredWidth(),mWebView.getMeasuredHeight(),Bitmap.Config.ARGB_8888);Canvascanvas=newCanvas(longImage);//画布的宽高与WebView的网页一致Paintpaint=newPaint();canvas.drawBitmap(longImage,0,mWebView.getMeasuredHeight(),paint);mWebView.draw(画布);Android为了提高滚动等方面的绘制速度,可以为每个View创建一个缓存,使用View#buildDrawingCache为自己的View建立一个对应的缓存,这个缓存就是一个位图对象。使用该函数可以对整个屏幕view进行截图并生成Bitmap,也可以获取指定View的Bitmap对象。这里因为需要在原图上绘制Logo,所以直接使用了WebView的draw方法。由于我们的H5页面大多运行在微信的X5浏览器中,为了减少前端的适配工作,我们在Android项目中引入了腾讯的X5浏览器内核来替代系统原生的WebView内核。关于X5会有专门的文章介绍内核,敬请期待。这里需要说明一下如何在X5内核下生成长Web图片。上面的代码展示了系统原生的WebView图片生成方案,但是上面的代码在X5环境下失效了。踩坑查看X5内核源码后,终于发现为了解决这个问题,下面用关键代码来说明具体实现。//这里的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();BitmaplongImage=Bitmap.createBitmap(mWebView.getMeasuredWidth(),mWebView.getMeasuredHeight()+endHeight,Bitmap.Config.ARGB_8888);Canvascanvas=newCanvas(longImage);//画布的宽高与WebView的网页一致Paintpaint=newPaint();canvas.drawBitmap(longImage,0,mWebView.getMeasuredHeight(),paint);floatscale=getResources().getDisplayMetrics().density;x5Bitmap=Bitmap.createBitmap(mWebView.getWidth(),mWebView.getHeight(),Bitmap.Config.ARGB_8888);Canvasx5Canvas=newCanvas(x5Bitmap);x5Canvas.drawColor(ContextCompat.getColor(this,R.color.fragment_default_background));mWebView.getX5WebViewExtension().snapshotWholePage(x5Canvas,false,false);//没有这行代码无法正常生成长图Matrixmatrix=newMatrix();matrix.setScale(scale,scale);longCanvas.drawBitmap(x5Bitmap,matrix,paint);注意:生成的长图分辨率X5内核比NativeWebView差,目前没有很好的解决办法。再见。但是也有例外,比如微博的长图,锤子笔记的长图等,如果直接通过微信分享SDK或者微博分享SDK分享这些图片,你会发现图片基本都是模糊的,但是图片发到iphone上也能正常查看,只能感叹安卓版微信的力不从心了。微信SDK不强大,但产品体验还是不能丢的。我应该怎么办?还是有办法的。我们都知道,除了各个社交平台的分享SDK,系统还提供了原生的分享解决方案。本质上,社交平台将目标Activity暴露给外界。然后第三方APP可以根据预先定义的Intent跳转规则调用社交平台,同时完成数据传输和展示。貌似可以彻底解决问题,但是还是有坑需要踩。在Android7.0及以上版本,系统限制了以file://开头的Intent传输数据,也限制了系统原生的单图分享。我应该怎么办?两种解决方案,一种是在7.0及以上使用微信等分享SDK,接受分享图片模糊的现状,另一种是跳过系统对Intent中file://开头文件传输的限制通过反思,但这种方法会有风险。毕竟,我们不知道Android未来会做什么。要做哪些调整。以下是跳过系统限制的代码片段以供参考。if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){try{Methodddfu=StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure");ddfu.invoke(null);}catch(Exceptione){}}目前基本OK满足任何图片尺寸的分享。另外经验证发现微信Android版SDK对缩略图和分享图片的大小有限制。官方指导是缩略图小于32K,分享图片小于10M可以正常分享。这是理论上限,不要太接近这个上限,如果图片太大,缩略图和分享的图片会模糊,甚至不能正常分享,当然没有这个限制通过系统分享,图片比较清晰。除了图片大小限制,缩略图大小也有限制,官方文档中没有给出。测试结果表明,图片尺寸小于等于120×120是一个比较安全的范围,分享没有问题。4.总结截屏监控,WebView生成长图,长图分享,这些都是我们团队从来没有遇到过的业务需求。在满足产品业务需求的同时,我们也踩了很多坑,积累了一些经验,特此总结。
