Android App内截屏监控及涂鸦功能实现

Android截屏功能是一个常用的功能,可以方便的用来分享或者发送给好友,本文介绍了如何实现app内截屏监控功能,当发现用户在我们的app内进行了截屏操作时,将自动展示该截屏,并提供用户随意圈点涂鸦,添加马赛克,撤销,分享等功能。

本文GitHub源码地址
实现效果如下:

实现该功能有以下技术需求:

1.   当app在前台展示的时候能够自动监听用户在app内的截屏,当app进入后台,停止监听
2.  监听到截屏后展示该截屏,并提供涂鸦(包括随意圈点和敏感信息马赛克)和上传分享功能
3.  涂鸦的每一步都可以撤销

涉及如下知识点:

1. App内截屏监听
2. 大图压缩
3. ImageView尺寸自适应
4. 自定义View实现涂鸦功能
5. 涂鸦撤销操作

对于截图监听有两种常用方案,方案一是通过FileObserver监听截屏文件夹,当有新的截屏文件产生时,调用设定的回调函数执行相关操作。该方案优缺点如下:
优点:

1. 实现简单

缺点:

1. 不同手机默认的截屏路径可能不同,需要做适配处理
2. 不同手机截屏触发的事件名称可能不同,需要测试适配
3. 监听到截屏事件后马上获取图片获取不到,需要延迟一段时间

方案二是通过ContentObserver监听多媒体图片库资源的变化。当手机上有新的图片文件产生时都会通过MediaProvider类向图片数据库插入一条记录,以方便系统的图片库进行图片查询,可以通过ContentObserver接收图片插入事件,并获取插入图片的URI。
优点:

1. 不同手机触发的事件是一样的

缺点:

1. 不同手机截屏文件的前缀可能不同,需要做适配
2. 监听到截屏事件后马上获取图片获取不到,需要延迟一段时间

这两种方式都需要根据手机做适配,第一种方式可以控制截屏监控只在App前台展示的时候进行,操作简单,我们使用这种方式做截屏监控。

接下来通过代码介绍具体实现。

FileObserver通过startWatching/stopWatching方法进行启动/停止文件监控,我们在BaseActivity的onResume和onPause方法中分别调用两个方法,其他Activity继承BaseActivity,实现App进入前台开始监控,转入后台停止监控的效果。

BaseActivity.java

public class BaseActivity extends AppCompatActivity {@Overrideprotected void onResume() {super.onResume();//  设置回调函数FileObserverUtils.setSnapShotCallBack(new SnapShotTakeCallBack(this));FileObserverUtils.startSnapshotWatching();}@Overrideprotected void onPause() {super.onPause();FileObserverUtils.stopSnapshotWatching();}
}

通过setSnapShotCallBack设置回调函数,并进行FileObserver初始化:

public class FileObserverUtils {...public static void setSnapShotCallBack(ISnapShotCallBack callBack) {snapShotCallBack = callBack;initFileObserver();}private static void initFileObserver() {SNAP_SHOT_FOLDER_PATH = Environment.getExternalStorageDirectory()+ File.separator + Environment.DIRECTORY_PICTURES+ File.separator + "Screenshots" + File.separator;fileObserver = new FileObserver(SNAP_SHOT_FOLDER_PATH, FileObserver.CREATE) {@Overridepublic void onEvent(int event, String path) {if (null != path && event == FileObserver.CREATE && (!path.equals(lastShownSnapshot))){lastShownSnapshot = path; // 有些手机同一张截图会触发多个CREATE事件,避免重复展示String snapShotFilePath = SNAP_SHOT_FOLDER_PATH + path;int tryTimes = 0;while (true) {try { // 收到CREATE事件后马上获取并不能获取到,需要延迟一段时间Thread.sleep(600);} catch (Exception e) {e.printStackTrace();}try {BitmapFactory.decodeFile(snapShotFilePath);break;} catch (Exception e) {e.printStackTrace();tryTimes++;if (tryTimes >= MAX_TRYS) { // 尝试MAX_TRYS次失败后,放弃return;}}}snapShotCallBack.snapShotTaken(path);}}};}...
}

FileObserver初始化传入要监控的截屏图片文件夹路径,当该文件夹下面的文件发生变化,包括截图生成新的图片时,调用onEvent函数,传入event和文件的path。我们根据event过滤出截屏事件,在这里是FileObserver.CREATE事件。收到事件后马上获取截图是获取不到的,需要过几百毫秒才能获取到,这里会让线程sleep一段时间再尝试获取,重试两次如果还获取失败就放弃。获取成功的话调用设置好的回调函数进行下一步操作。

我们的回调函数很简单,就是打开一个用于展示截屏的新的Activity叫SnapShotEditActivity,并传入截屏路径:

public class SnapShotTakeCallBack implements ISnapShotCallBack {public static final String SNAP_SHOT_PATH_KEY = "snap_shot_path_key";private Context context;public SnapShotTakeCallBack(Context context) {this.context = context;}@Overridepublic void snapShotTaken(String path) {Intent intent = new Intent(context, SnapShotEditActivity.class);intent.putExtra(SNAP_SHOT_PATH_KEY, path);context.startActivity(intent);}
}

该Activity界面如下:

通过圈出问题随意圈点,通过马赛克覆盖隐私信息,回退一步可以撤销之前的操作。

我们使用一个自定义View实现涂鸦的功能:

public class PaintableImageView extends ImageView {private List<LineInfo> lineList; // 线条列表private LineInfo currentLine; // 当前线条private LineInfo.LineType currentLineType = LineInfo.LineType.NormalLine; // 当前线条类型private Paint normalPaint = new Paint();private static final float NORMAL_LINE_STROKE = 5.0f;private Paint mosaicPaint = new Paint();private static final int MOSAIC_CELL_LENGTH = 30; // 马赛克每个大小40*40像素,共三行private Drawable drawable;private Bitmap bitmap;private boolean mosaics[][]; // 马赛克绘制中用于记录某个马赛克格子的数值是否计算过private int mosaicRows; // 马赛克行数private int mosaicColumns; // 马赛克列数{lineList = new ArrayList<>();normalPaint.setColor(Color.RED);normalPaint.setStrokeWidth(NORMAL_LINE_STROKE);}public PaintableImageView(Context context) {super(context);}public PaintableImageView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);}public PaintableImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}/*** 设置线条类型* @param type*/public void setLineType(LineInfo.LineType type) {currentLineType = type;}@Overridepublic boolean onTouchEvent(MotionEvent event) {float xPos = event.getX();float yPos = event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:currentLine = new LineInfo(currentLineType);currentLine.addPoint(new PointInfo(xPos, yPos));lineList.add(currentLine);invalidate();return true; // return true消费掉ACTION_DOWN事件,否则不会触发ACTION_UPcase MotionEvent.ACTION_MOVE:currentLine.addPoint(new PointInfo(xPos, yPos));invalidate();return true;case MotionEvent.ACTION_UP:currentLine.addPoint(new PointInfo(xPos, yPos));invalidate();break;}return super.onTouchEvent(event);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);for (int i = 0; i < mosaicRows; i++) {for (int j = 0; j < mosaicColumns; j++) {mosaics[i][j] = false;}}for (LineInfo lineinfo : lineList) {if (lineinfo.getLineType() == LineInfo.LineType.NormalLine) {drawNormalLine(canvas, lineinfo);} else if (lineinfo.getLineType() == LineInfo.LineType.MosaicLine) {drawMosaicLine(canvas, lineinfo);}}}/*** 绘制马赛克线条* @param canvas* @param lineinfo*/private void drawMosaicLine(Canvas canvas, LineInfo lineinfo) {if (null == bitmap) {init();}if (null == bitmap) {return;}for (PointInfo pointInfo : lineinfo.getPointList()) {// 对每一个点,填充所在的小格子以及上下两个格子(如果有上下格子)int currentRow = (int) ((pointInfo.y -1) / MOSAIC_CELL_LENGTH);int currentCol = (int) ((pointInfo.x -1) / MOSAIC_CELL_LENGTH);fillMosaicCell(canvas, currentRow, currentCol);fillMosaicCell(canvas, currentRow - 1, currentCol);fillMosaicCell(canvas, currentRow + 1, currentCol);}}/*** 填充一个马赛克格子* @param cavas* @param row 马赛克格子行* @param col 马赛克格子列*/private void fillMosaicCell(Canvas cavas, int row, int col) {if (row >= 0 && row < mosaicRows && col >= 0 && col < mosaicColumns) {if (!mosaics[row][col]) {mosaicPaint.setColor(bitmap.getPixel(col * MOSAIC_CELL_LENGTH, row * MOSAIC_CELL_LENGTH));cavas.drawRect(col * MOSAIC_CELL_LENGTH, row * MOSAIC_CELL_LENGTH, (col + 1) * MOSAIC_CELL_LENGTH, (row + 1) * MOSAIC_CELL_LENGTH, mosaicPaint);mosaics[row][col] = true;}}}/*** 绘制普通线条* @param canvas* @param lineinfo*/private void drawNormalLine(Canvas canvas, LineInfo lineinfo) {if (lineinfo.getPointList().size() <= 1) {return;}for (int i = 0; i < lineinfo.getPointList().size() - 1; i++) {PointInfo startPoint  = lineinfo.getPointList().get(i);PointInfo endPoint  = lineinfo.getPointList().get(i + 1);canvas.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y, normalPaint);}}/*** 初始化马赛克绘制相关*/private void init() {drawable = getDrawable();try {bitmap = ((BitmapDrawable)drawable).getBitmap();} catch (ClassCastException e) {e.printStackTrace();return;}mosaicColumns = (int)Math.ceil(bitmap.getWidth() / MOSAIC_CELL_LENGTH);mosaicRows = (int)Math.ceil(bitmap.getHeight() / MOSAIC_CELL_LENGTH);mosaics = new boolean[mosaicRows][mosaicColumns];}/*** 删除最后添加的线*/public void withDrawLastLine() {if (lineList.size() > 0) {lineList.remove(lineList.size() - 1);invalidate();}}/*** 判断是否可以继续撤销* @return*/public boolean canStillWithdraw() {return lineList.size() > 0;}
}

该自定义View继承自ImageView,通过onTouchEvent获取要绘制的线条,MotionEvent.ACTION_DOWN/ACTION_UP标志一条线的起止,用数组保存所有的线条,每条线是数组的一个元素,记录了改线上面的所有点和线条的类型,是普通线条还是马赛克线条。然后通过onDraw在Canvas上进行绘制。

绘制过程根据线条类型调用不同的绘制方法,普通绘制调用drawNormalLine通过canvas.drawLine进行,马赛克绘制调用drawMosaicLine进行。马赛克绘制思路是首先将截图分割成若干个大小相同的格子,判断每个点落在哪个格子里,绘制该格子和上下两个格子,每个格子的颜色采用格子左上角的像素颜色填充,实现马赛克效果。为了避免相邻的点所在的格子重复绘制,采用一个二维数组标志某个格子是否被绘制过,只绘制尚未绘制过的格子。

撤销上一步只需要将数组中最后一条记录删除,重绘即可。

由于SnapShotEditActivity中图片布局的高度是未知的,需要在布局加载完成后才能获取,这里我们通过ViewTreeObserver的addOnGlobalLayoutListener实现:

public class SnapShotEditActivity extends AppCompatActivity {...@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {...imageView = (PaintableImageView) findViewById(R.id.image_view);ViewTreeObserver viewTreeObserver = imageView.getViewTreeObserver();viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {@Overridepublic void onGlobalLayout() {// 自适应调整图片空间大小,并根据其大小压缩图片autoFitImageView();ViewTreeObserver vto = imageView.getViewTreeObserver();if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {vto.removeOnGlobalLayoutListener(this);} else {vto.removeGlobalOnLayoutListener(this);}}});...}...
}

其中通过autoFitImageView()实现ImageView尺寸的自适应调整,并根据ImageView的尺寸压缩截图,避免出现OOM。

private void autoFitImageView() {int imageViewHeight = imageView.getHeight(); Bitmap compressedBitmap = BitmapUtils.getCompressedBitmap(SNAP_SHOT_FOLDER_PATH + snapShotPath, imageViewHeight);if (null != compressedBitmap) {LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(compressedBitmap.getWidth(), compressedBitmap.getHeight());layoutParams.gravity = Gravity.CENTER;imageView.setLayoutParams(layoutParams);imageView.requestLayout();imageView.setImageBitmap(compressedBitmap);}
}

在onCreate里直接调用imageView.getHeight()返回的是0,因为此时还没完成空间的加载,放在onGlobalLayout里面可以正确的获取宽高。

getCompressedBitmap返回一个跟ImageView宽高一样的压缩过的Bitmap。

public static Bitmap getCompressedBitmap(String filePath, int needHeight) {try {BitmapFactory.Options o = new BitmapFactory.Options();// 第一次只解码原始长宽的值o.inJustDecodeBounds = true;try {BitmapFactory.decodeStream(new FileInputStream(new File(filePath)), null, o);} catch (FileNotFoundException e) {e.printStackTrace();return null;}BitmapFactory.Options o2 = new BitmapFactory.Options();// 根据原始图片长宽和需要的长宽计算采样比例,必须是2的倍数,//  IMAGE_WIDTH_DEFAULT=768, IMAGE_HEIGHT_DEFAULT=1024int needWidth = (int) (needHeight * 1.0 / o.outHeight * o.outWidth);o2.inSampleSize = 2;// 每像素采用RGB_565的格式保存o2.inPreferredConfig = Bitmap.Config.RGB_565;// 根据压缩参数的设置进行第二次解码Bitmap b = BitmapFactory.decodeStream(new FileInputStream(new File(filePath)), null, o2);Bitmap scaledBitmap = Bitmap.createScaledBitmap(b, needWidth, needHeight, true);//          b.recycle();  // b.recycle will cause prev Bitmap.createScaledBitmap null pointer exception on b occasionallySystem.gc();return scaledBitmap;} catch (Exception e) {e.printStackTrace();}return null;}

这里如果直接调用Bitmap.createScaledBitmap生成指定尺寸的Bitmap有可能会因为传入的bitmap过大导致OOM,所以要先压缩一遍,装进内存后再调用Bitmap.createScaledBitmap生成指定大小的Bitmap。同时,之前想尝试设定BitmapFactory.Options的outWidth/outHeight参数为指定的宽高,同时inJustDecodeBounds=false的方式来生成指定大小的bitmap,发现不可行。必须使用Bitmap.createScaledBitmap才能生成指定宽高的Bitmap。

这样就实现了App内截屏监听,展示,涂鸦,马赛克,撤销等操作,思路不难,不过要注意的细节不少,同时需要在不同机型上测试适配才能保证稳定性。

Android App内截屏监控及涂鸦功能实现相关推荐

  1. Android app内截屏监听

    1.在 Application onCreate 方法设置 activity 生命周期监听 package com.example.myscreenshot;import android.app.Ac ...

  2. 2022-10-09 Android app禁止截屏方法 和 在禁止截屏的情况下录制屏幕

    一.APP有时候为了保护用户的隐私安全会禁止用户录屏和截屏,比如金融类的app等.可以在app的onCreate方法中添加这么一段代码 1.代码 //禁止app录屏和截屏getWindow().set ...

  3. android App内监听截图加二维码

    Android截屏功能是一个常用的功能,可以方便的用来分享或者发送给好友,本文介绍了如何实现app内截屏监控功能,当发现用户在我们的app内进行了截屏操作时,进行对图片的二次操作,例如添加二维码,公司 ...

  4. 应用内截屏的代码,在Activity中测试可用

    截屏功能让我十分头疼,想做个无需root的又找不到资料.这里暂且分享一个无需root的,在应用内截屏的代码,本文转自:http://blog.csdn.net/csh159/article/detai ...

  5. Android 4.0 截屏(Screenshot)代码流程小结

    Android 4.0 截屏 在Android 4.0 之前,Android手机上如果要使用截屏功能,只能通过Root手机,且使用第3方截图软件来实现截屏功能. Android4.0中,系统自带了截屏 ...

  6. android 8.1 截屏,Android8.1 MTK平台 截屏功能分析

    前言 涉及到的源码有 frameworksbaseservicescorejavacomandroidserverpolicyPhoneWindowManager.java vendormediate ...

  7. Android 4.0 截屏(Screenshot)

    Android 4.0 截屏(Screenshot)代码流程小结 参考文档:Android 4.0 截屏(Screenshot)代码流程小结:http://blog.csdn.net/hk_256/a ...

  8. android长截图工具下载,Android实现长截屏功能

    本文实例为大家分享了Android实现长截屏功能的具体代码,供大家参考,具体内容如下 1.MainActivity public class MainActivity extends AppCompa ...

  9. 禁止APP内部截屏,系统截屏都不好使

    有时候为了信息安全考虑,我们会对信息的安全性有着严格的限制,APP内禁止截屏就是其中一种,只需要在主Activity的super.onCreate(savedInstanceState)之后新增一下一 ...

最新文章

  1. 在虚拟机中安装Ubuntu Server 15.04
  2. Exchange系列课程之三--群集环境中安装Exchange Server 2003
  3. 内核ioread,iowrite volatie 的正确使用
  4. C++union 联合
  5. PID控制器开发笔记(转)
  6. c语言实现通讯录_C语言实现双人猜数字游戏
  7. linux下编辑文件实验,Linux实验_修改
  8. python对象的深复制与浅复制
  9. 【算法笔记】输出字符串的所有子序列
  10. 忘记压缩包密码 python 暴力破解rar密码
  11. 英文版权声明_想避免版权问题,这些网站你一定需要
  12. SegmentFault 思否发布开源问答社区软件 Answer
  13. python-docx 设置Table 边框样式、单元格边框样式
  14. c语言backtrack算法6,一个关于数组回溯算法(backtrack)的通用模式
  15. html写文章发布,写文章.html
  16. 我用了九个小时给高中同学写了一款留言板
  17. leetcode:6080. 使数组按非递减顺序排列【单调栈 + 合并】
  18. android更新UI(界面)的方法;android刷新界面数据的方法;android定时器更新界面
  19. NTLDR is missing
  20. 怎么把window系统下的文件传到Ubuntu里去呢?

热门文章

  1. docker部署OpenVAS开源漏洞扫描系统——筑梦之路
  2. 嵌入式linux/鸿蒙开发板(IMX6ULL)开发(二十四)具体单板的GPIO操作方法
  3. 4K秒开,稀缺宝藏影视APP!
  4. 反相畴的基础知识和一篇论文
  5. 服务器运营维护要多少钱,运营维护升级也需成本
  6. 游戏机生产厂家不朽的神迹碎片系统详细攻略心得
  7. 【offerMe--面经必备】---京东面经分享(包含答案)
  8. CRM对接企业微信日程快速实现提醒功能
  9. SAP-MM知识精解-自动科目记账(04-2)- 业务事物之“科目分组代码”的影响
  10. 一起领略css3动画的强大