原文:https://youzanmobile.github.io/2017/05/19/android-screenshot-and-webview/

最近在做新业务需求的同时,我们在 Android 上遇到了一些之前没有碰到过的问题,截屏分享、 WebView 生成长图以及长图在各个分享渠道分享时图片模糊甚至分享失败等问题,在这过程中踩了很多坑,到目前为止绝大部分的问题都还算是有了比较满意的解决方案。以下就从三个方面来总结一下过程中遇到的挑战和最后的解决方案。

一、概述

最近在做新业务需求的同时,我们在 Android 上遇到了一些之前没有碰到过的问题,截屏分享、 WebView 生成长图以及长图在各个分享渠道分享时图片模糊甚至分享失败等问题,在这过程中踩了很多坑,到目前为止绝大部分的问题都还算是有了比较满意的解决方案。以下就从三个方面来总结一下过程中遇到的挑战和最后的解决方案。

二、截图分享

在 Android 原生系统中是没有提供截图的广播或者监听事件的,也就是说代码层面无法获知用户的截屏操作,这样就无法满足用户截屏后跳出分享提示的需求。既然无法从根本上解决截屏监听的问题,那么就要考虑通过其他方式间接实现,目前比较成熟稳定的方案是监听系统媒体数据库资源的变化,具体方案原理如下:

Android 系统有一个媒体数据库,每拍一张照片,或使用系统截屏截取一张图片,都会把这张图片的详细信息加入到这个媒体数据库,并发出内容改变通知,我们可以利用内容观察者(ContentObserver)监听媒体数据库的变化,当数据库有变化时,获取最后插入的一条图片数据,如果该图片符合特定的规则,则认为被截屏了。

考虑到手机存储包括内部存储器和外部存储器,为了增强兼容性,最好同时监听两种储存空间的变化,以下是需要 ContentObserver 监听的资源 URI :

      
1
2
      
MediaStore.Images.Media.INTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI

读取外部存储器资源,需要添加权限:

      
1
      
android.permission.READ_EXTERNAL_STORAGE

注:在 Android 6.0 及以上版本需要动态申请权限

1. 截屏判断规则

当 ContentObserver 监听到媒体数据库的数据改变, 在有数据改变时获取最后插入数据库的一条图片数据, 如果符合以下规则, 则认为截屏了:

  1. 时间判断:通常截屏生成后会立马存入系统多媒体数据库,也就是说监听到数据库变化的时间与截图生成的时间不会相差太多,这里推荐以10秒作为阈值,当然这个也是经验值。
  2. 尺寸判断:截屏顾名思义取得是当前手机屏幕尺寸大小的图片,所以图片宽高大于屏幕宽高的肯定都不是截图产生的。
  3. 路径判断:由于各手机厂家存放截图的文件路径都不太一样,国内情况可能会更严重,但是通常图片保存路径都会包含一些常见的关键词,比如 “screenshot”、 “screencapture” 、 “screencap” 、 “截图”、 “截屏”等,每次都检查图片路径信息是否包含这些关键词。

关于第3点需要补充说明一下,由于要判断图片文件路径是否包含关键字,所以目前仅支持中英文环境,如果需要支持其他语言,需要手动添加一些该语言的关键词,否则有可能获取不到图片。

以上3点基本上可以保证截图的正常监听,当然在实际测试过程中,还会发现有些机型存在多报的情况,所以还需要做一些去重等工作,关于去重下面还会再提及。

2. 关键代码

原理都了解清楚了,那么接下来就是如何实现的问题了。这里最关键是媒体内容观察者的设置,从数据库中取出第一条数据并解析图片信息,然后再检验图片信息是否符合以上3条规则。

为了说清楚如何监听媒体数据库改变,先要稍微讲一下 ContentObserver 的原理。 ContentObserver ——内容观察者,目的是观察(捕捉)特定 Uri 引起的数据库的变化,继而做一些相应的处理,它类似于数据库技术中的触发器(Trigger),当 ContentObserver 所观察的 Uri 发生变化时,便会触发它。当然想要观察就必须先要注册, Android 系统提供了 ContentResolver#registerContentObserver 方法用来注册观察器。此部分不熟悉的同学可以温习一下 Android 的 ContentProvider 相关知识。

接下来直接用代码说明整个注册和触发流程,代码如下:

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
      
private void initMediaContentObserver() {
// 运行在 UI 线程的 Handler, 用于运行监听器回调
private final Handler mUiHandler = new Handler(Looper.getMainLooper());
// 创建内容观察者,包括内部存储和外部存储
mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler);
mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler);
// 注册内容观察者
mContext.getContentResolver().registerContentObserver(
MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, mInternalObserver);
mContext.getContentResolver().registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver);
}
/**
* 自定义媒体内容观察者类(观察媒体数据库的改变)
*/
private class MediaContentObserver extends ContentObserver {
private Uri mediaContentUri; // 需要观察的Uri
public MediaContentObserver(Uri contentUri, Handler handler) {
super(handler);
mediaContentUri = contentUri;
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
// 处理媒体数据库反馈的数据变化
handleMediaContentChange(mediaContentUri);
}
}

有注册就需要在 Activity 销毁时取消注册,所以还需要封装一个解除注册的方法供外部调用, Android 系统提供 ContentResolver#unregisterContentObserver 方法来取消注册,代码比较简单,这里就不再展示了。

监听器设置和注册完成后,一旦用户操作了截屏动作,系统就会执行 ContentObserver#onChange 回调方法,在这个方法中我们可以根据 Uri 获取并解析数据。这里展示一下具体的数据解析过程,上述提到的规则判断比较简单,就不再展示了。

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
      
private void handleMediaContentChange(Uri contentUri) {
Cursor cursor = 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 + " desc limit 1");
if (cursor == null) return;
if (!cursor.moveToFirst()) return;
// cursor.getColumnIndex获取数据库列索引
int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
String data = cursor.getString(dataIndex); // 图片存储地址
int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
long dateTaken = cursor.getLong(dateTakenIndex); // 图片生成时间
int width = 0;
int height = 0;
if (Build.VERSION.SDK_INT >= 16) {
int widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH);
int heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT);
width = cursor.getInt(widthIndex); // 获取图片高度
height = cursor.getInt(heightIndex); // 获取图片宽度
} else {
Point size = getImageSize(data); // 根据路径获取图片宽和高
width = size.x;
height = size.y;
}
// 处理获取到的第一行数据,分别判断路径是否包含关键词、时间差以及图片宽高和屏幕宽高的大小关系
handleMediaRowData(data, dateTaken, width, height);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
}

有些手机 ROM 截屏一次会发出多次内容改变的通知,因此需要做去重操作,去重也不复杂,可以用列表缓存最近十几条图片地址数据,每次获取到新的图片地址,都会先判断缓存中是否存在相同的图片地址,如果当前的图片地址已经存在列表中,则直接过滤掉即可,否则添加到缓存中。如此就可以保证截屏监听事件既不遗漏也不重复。

以上就是手机截屏的核心原理和关键代码,如果需要分享截屏图片也很简单, data 即为图片的存储地址,转换成 Bitmap 即可完成分享。

二、WebView 生成长图

介绍 web 长图之前,先来说一下单屏图片的生成方案,和手机截图不同的是生成的图片不会显示顶部的状态栏、标题栏以及底部的菜单栏,可以满足不同的业务需求。

      
1
2
3
4
5
6
      
// WebView 生成当前屏幕大小的图片,shortImage 就是最终生成的图片
Bitmap shortImage = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(shortImage); // 画布的宽高和屏幕的宽高保持一致
Paint paint = new Paint();
canvas.drawBitmap(shortImage, screenWidth, screenHeight, paint);
mWebView.draw(canvas);

有的时候我们需要将一个长 Web 网页生成图片分享出去,相似的例子就是手机端的各种便签应用,当便签内容超出一屏时,就需要将所有的内容生成一张长图对外分享出去。

WebView 和其他 View 一样,系统都提供了 draw 方法,可以直接将 View 的内容渲染到画布上,有了画布我们就可以在上面绘制其他各种各种的内容,比如底部添加 Logo 图片,画红线框等等。关于 WebView 生成长图网上已经有很多现成的方案和代码,以下代码是经测试过的稳定版本,供参考。

      
1
2
3
4
5
6
7
8
9
10
11
12
13
      
// 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();
Bitmap longImage = Bitmap.createBitmap(mWebView.getMeasuredWidth(),
mWebView.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(longImage); // 画布的宽高和 WebView 的网页保持一致
Paint paint = new Paint();
canvas.drawBitmap(longImage, 0, mWebView.getMeasuredHeight(), paint);
mWebView.draw(canvas);

Android 为了提高滚动等各方面的绘制速度,可以为每一个 View 建立一个缓存,使用 View#buildDrawingCache 为自己的 View 建立相应的缓存, 这个 cache 就是一个 bitmap 对象。利用这个功能可以对整个屏幕视图进行截屏并生成 Bitmap ,也可以获得指定的 View 的 Bitmap 对象。这里由于还要在原有的图片上绘制 Logo ,所以直接使用了 WebView 的 draw 方法了。

由于我们的 H5 页面大部分都是运行在微信的 X5 浏览器中,所以为了减少前端的适配工作,我们将腾讯的 X5 浏览器内核引入了 Android 工程中,代替系统原生的 WebView 内核,关于 X5 内核的引入后续还会有专门的文章介绍,敬请期待。

这里需要说明一下如何在 X5 内核下生成 Web 长图,上面代码展示的系统原生 WebView 生成图片的方案,但是在 X5 环境下上述代码就失效了,经过踩坑以及查看 X5 内核源代码,最终我们找到了解决该问题的方法,下面用关键代码来说明一下具体的实现方式。

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
      
// 这里的 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();
Bitmap longImage = Bitmap.createBitmap(mWebView.getMeasuredWidth(),
mWebView.getMeasuredHeight() + endHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(longImage); // 画布的宽高和 WebView 的网页保持一致
Paint paint = new Paint();
canvas.drawBitmap(longImage, 0, mWebView.getMeasuredHeight(), paint);
float scale = getResources().getDisplayMetrics().density;
x5Bitmap = Bitmap.createBitmap(mWebView.getWidth(), mWebView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas x5Canvas = new Canvas(x5Bitmap);
x5Canvas.drawColor(ContextCompat.getColor(this, R.color.fragment_default_background));
mWebView.getX5WebViewExtension().snapshotWholePage(x5Canvas, false, false); // 少了这行代码就无法正常生成长图
Matrix matrix = new Matrix();
matrix.setScale(scale, scale);
longCanvas.drawBitmap(x5Bitmap, matrix, paint);

注:X5 内核生成的长图清晰度比原生 WebView 要差一些,目前还没有太好的解决方案。

三、长图分享

一般我们向各个社交平台上发送的图片都比较小,最大也就是手机屏幕大小的图片,再大的就不多见了。但是也有例外,比如微博的长图、锤子便签的长图等等,如果直接将这些图片通过微信分享 SDK 或者微博分享 SDK 分享出去,就会发现图片基本上都是模糊的,但是将图片发送给 iPhone 手机就可以正常查看,我们只能哀叹 Android 版微信不给力。

微信 SDK 不给力,但是产品体验还是不能丢,怎么办呢?办法还是有的,我们都知道除了各个社交平台自己的分享 SDK ,系统提供了原生分享方案,本质上就是社交平台把目标 Activity 对外暴露了出来,然后第三方 App 就可以根据事先定义好的 Intent 跳转规则唤起社交平台,同时完成数据传输和展示。

好像问题可以完美解决了,但是还是有坑需要接着踩。在 Android 7.0 及以上的版本系统限制了 Intent 传输 file:// 开头的数据,这也就限制了系统原生分享单图,怎么办呢?两种方案,一种是在 7.0 及以上版本上使用微信等分享 SDK ,接受分享图片模糊的现状,另一种是通过反射跳过系统对以 file:// 开头文件在 Intent 中传输的限制,但是这种方式会有风险,毕竟我们不知道未来 Android 会做出什么调整。以下是跳过系统限制的代码片段,供参考。

      
1
2
3
4
5
6
7
8
      
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
Method ddfu = StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure");
ddfu.invoke(null);
} catch (Exception e) {
}
}

至此基本上可以满足任意图片大小的分享了。此外经过验证还发现微信分享 Android 版 SDK 对缩略图和分享图的大小都有限制,官方给的指导意见是缩略图小于 32K ,分享图片小于 10M 即可正常分享,但是试验下来这两个值都是理论上限,不要太接近这个上限,如果图片太大,缩略图和分享图都会出现模糊的情况,甚至无法正常分享,当然对于通过系统分享的话就不存在这个限制,图片也比较清晰。

除了图片大小有限制,缩略图的尺寸也是有限制的,这一点官方文档并没有给出,试验结果显示图片尺寸小于等于120x120是比较安全的范围,分享都没有问题。

四、小结

截屏监听、 WebView 生成长图以及长图分享都是我们团队之前未曾遇到过的业务需求,在满足产品业务需求的同时,也踩了很多坑,积累了一些经验,特此总结。

Android截屏与WebView长图分享经验总结【转】相关推荐

  1. Android 截屏与 WebView 长图分享经验总结

    最近在做新业务需求的同时,我们在 Android 上遇到了一些之前没有碰到过的问题,截屏分享. WebView 生成长图以及长图在各个分享渠道分享时图片模糊甚至分享失败等问题,在这过程中踩了很多坑,到 ...

  2. Android 截屏监听(截图分享功能实现)

    具体来说就是,检测到了用户在应用内有截图,弹出一个分享界面, 在截图下方添加一个二维码,进行分享. ●●●  前言 Android系统没有直接对截屏事件监听的接口,也没有广播,只能自己动手来丰衣足食, ...

  3. android 截屏 效率,android 截屏以及对该图进行模糊

    由于项目中用得比较多的两个功能 截图 和 模糊,我就整理成一个项目来概述. 一 .截图 核心代码 View.setDrawingCacheEnabled(true); View.buildDrawin ...

  4. Android截屏截图方法汇总(Activity、View、ScrollView、ListView、RecycleView、WebView截屏截图)

    Android截屏 Android截屏的原理:获取具体需要截屏的区域的Bitmap,然后绘制在画布上,保存为图片后进行分享或者其它用途 一.Activity截屏 1.截Activity界面(包含空白的 ...

  5. Android截屏分享

    最近项目需要实现Android截屏分享功能,包括Android截屏获取图片.将图片保存到本地.通知系统相册更新.通过微信.QQ.微博分享截屏图片,本篇博客作为总结回顾. 一.Android截屏获取图片 ...

  6. 谷歌/360浏览器截长图分享

    谷歌/360浏览器截长图分享 谷歌浏览器截长图 这些都是亲身实践过的,起因是领导给了两个禅道的网址给我,让我将里面的内容截图保存下来,但是内容不止一页,内容很多,普通的截图肯定是行不通,然后在百度上找 ...

  7. Android截屏分享之View生成图片进行保存分享、全屏,半屏、指定VIew、弹窗.......

    Android截屏主要应用在分享这个操作,所有的截屏,截取的是视图.接下来给大家分享一下这个功能的干货 温馨提示:我这里分享使用的是极光的Jshare,也可以使用原生分享 这里是JshareSDK接入 ...

  8. ios android 截屏 分享,iOS微信截屏分享

    1.需求:將截屏后的圖片分享至微信好友或朋友圈. 2.問題:1.圖片縮略圖太大無法分享:2.分享的圖片不夠清晰. 3.描述:微信分享是需要設置兩張圖:需分享圖的縮略圖(大小有限制)和需分享的圖(要求高 ...

  9. Android截屏截图的几种方法总结

    Android截屏 Android截屏的原理:获取具体需要截屏的区域的Bitmap,然后绘制在画布上,保存为图片后进行分享或者其它用途 一.Activity截屏 1.截Activity界面(包含空白的 ...

最新文章

  1. BZOJ4939[Ynoi2016]掉进兔子洞(莫队+bitset)
  2. 【SpringMVC 之应用篇】 1_SpringMVC入门 —— 第一个 Spring MVC 程序
  3. Nginx 和 Zuul 的区别
  4. 360怎么看电脑配置_电脑速度慢怎么办?教你电脑速度慢的原因与解决方法
  5. 解放你的双手-Sql Server 2000智能提示工具[破解版]
  6. python中一切都是对象_python中一切皆对象
  7. matlab ode 实数,关于ode45中erf函数(输入必须为实数完全数的报错问题)
  8. 学习记录:python糗百爬虫
  9. 如何在计算机上增加一个磁盘分区,电脑怎么添加硬盘分区
  10. code runner 中文使用指南
  11. 领军服务外包 大连软件业加速对接资本市场
  12. DingTalk「开发者说」 5分钟开发钉钉应用
  13. python中使用什么命令安装组件_在离线环境下安装python组件
  14. 把握人性的弱点——正确处理人际关系
  15. 7-2 多分支表达-数据奇偶判断
  16. 2017秋招-技术岗-恒生电子
  17. c++11中的lock_guard和unique_lock使用浅析
  18. SEC储量基本知识 2021-04-21
  19. UNITY3D 动作游戏开发教程《怪物猎人》
  20. 连续周期信号的傅里叶级数(CFS)

热门文章

  1. 骚男用计算机的音乐,lol主播炸了77期骚男五杀什么背景音乐
  2. “中国星巴克”IPO在即,不过今年上市的独角兽基本都跪了...
  3. liunx 使用 zip 打包文件夹
  4. c# 跨网段扫描的方法
  5. python字体颜色代表什么_Python:字体颜色
  6. DGL官方教程--DGL图和节点/边的特征
  7. Kubernetes v1.23即将发布,有哪些重磅更新?
  8. 迭代器Iterator常用的方法
  9. 实验一:基于Ubuntu系统实现无人机自主飞行
  10. 怎么制作gif闪图?教你一招在线制作做gif闪图的方法