截屏实现方式和监听截屏详解

移动开发 Android
今天我们来介绍下Android里截屏方面的知识点介绍;通过获取DecorView的方式来实现截屏(前提是当前Activity已经加载完成),DecorView为整个Window界面的最顶层View,因此截屏不包含状态栏(SystemUI)部分.

[[433218]]

前言

今天我们来介绍下Android里截屏方面的知识点介绍;

一、Android截屏的方式

1、获取DecorView截屏

通过获取DecorView的方式来实现截屏(前提是当前Activity已经加载完成),DecorView为整个Window界面的最顶层View,因此截屏不包含状态栏(SystemUI)部分.

方式一

  1. View view = getWindow().getDecorView();     // 获取DecorView 
  2.    view.setDrawingCacheEnabled(true); 
  3.    view.buildDrawingCache(); 
  4.    Bitmap bitmap1 = view.getDrawingCache();    

方式二

  1. Bitmap bitmap2 = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); 
  2.    Canvas canvas = new Canvas(); 
  3.    canvas.setBitmap(bitmap2); 
  4.    view.draw(canvas); 

保存 bitmap1 或 bitmap2 均可保存截屏图片

2、调用系统源码截屏

由于是hide api,通过反射调用如下:

  1. public Bitmap takeScreenShot() { 
  2.         Bitmap bmp = null
  3.         mDisplay.getMetrics(mDisplayMetrics); 
  4.         float[] dims = {(float) mDisplayMetrics.widthPixels, (float) heightPixels}; 
  5.         float degrees = getDegreesForRotation(mDisplay.getRotation()); 
  6.         boolean requiresRotation = degrees > 0; 
  7.         if (requiresRotation) { 
  8.             mDisplayMatrix.reset(); 
  9.             mDisplayMatrix.preRotate(-degrees); 
  10.             mDisplayMatrix.mapPoints(dims); 
  11.             dims[0] = Math.abs(dims[0]); 
  12.             dims[1] = Math.abs(dims[1]); 
  13.         } 
  14.         try { 
  15.             Class<?> demo = Class.forName("android.view.SurfaceControl"); 
  16.             Method method = demo.getMethod("screenshot", new Class[]{Integer.TYPE, Integer.TYPE}); 
  17.             bmp = (Bitmap) method.invoke(demo, new Object[]{Integer.valueOf((int) dims[0]), Integer.valueOf((int) dims[1])}); 
  18.             if (bmp == null) { 
  19.                 return null
  20.             } 
  21.             if (requiresRotation) { 
  22.                 Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels, heightPixels, Bitmap.Config.RGB_565); 
  23.                 Canvas c = new Canvas(ss); 
  24.                 c.translate((float) (ss.getWidth() / 2), (float) (ss.getHeight() / 2)); 
  25.                 c.rotate(degrees); 
  26.                 c.translate((-dims[0] / 2), (-dims[1] / 2)); 
  27.                 c.drawBitmap(bmp, 0, 0, null); 
  28.                 c.setBitmap(null); 
  29.                 bmp.recycle(); 
  30.                 bmp = ss; 
  31.             } 
  32.             if (bmp == null) { 
  33.                 return null
  34.             } 
  35.             bmp.setHasAlpha(false); 
  36.             bmp.prepareToDraw(); 
  37.             return bmp; 
  38.         } catch (Exception e) { 
  39.             e.printStackTrace(); 
  40.             return bmp; 
  41.         } 
  42.     } 

二、截屏手机里的监听

利用FileObserver监听某个目录中资源变化情况;

利用ContentObserver监听全部资源的变化;

ContentObserver:通过ContentObserver监听图片多媒体的变化,当手机上有新图片文件产生时会通过MediaProvider类向图片数据库插入一条记录,监听图片插入事件来获得图片的URI;

今天讲的是通过ContentObserver实现;

1、ScreenShotHelper截屏帮助类

  1. /** 
  2.  * Description: 截屏帮助类 
  3.  */ 
  4. class ScreenShotHelper { 
  5.     companion object { 
  6.         const val TAG = "ScreenShotLog" 
  7.         /** 
  8.          * 读取媒体数据库时需要读取的列 
  9.          */ 
  10.         val MEDIA_PROJECTIONS = arrayOf( 
  11.             MediaStore.Images.ImageColumns.DATA, 
  12.             MediaStore.Images.ImageColumns.DATE_TAKEN 
  13.         ) 
  14.         /** 
  15.          * 读取媒体数据库时需要读取的列,其中 width、height 字段在 API 16 之后才有 
  16.          */ 
  17.         val MEDIA_PROJECTIONS_API_16 = arrayOf( 
  18.             MediaStore.Images.ImageColumns.DATA, 
  19.             MediaStore.Images.ImageColumns.DATE_TAKEN, 
  20.             MediaStore.Images.ImageColumns.WIDTH, 
  21.             MediaStore.Images.ImageColumns.HEIGHT 
  22.         ) 
  23.         /** 
  24.          * 截屏路径判断的关键字 
  25.          */ 
  26.         val KEYWORDS = arrayOf( 
  27.             "screenshot""screen_shot""screen-shot""screen shot"
  28.             "screencapture""screen_capture""screen-capture""screen capture"
  29.             "screencap""screen_cap""screen-cap""screen cap" 
  30.         ) 
  31.         fun showLog(msg: String) { 
  32.             Log.d(TAG, msg) 
  33.         } 
  34.     } 

2、监听器ScreenShotListener

  1. /** 
  2.  * Description: 截屏监听 
  3.  */ 
  4. class ScreenShotListener constructor(context: Context?) { 
  5.     private var mContext: Context 
  6.     private var mScreenRealSize: Point? = null 
  7.     private val mHasCallbackPaths: ArrayList<String> = ArrayList() 
  8.     private var mListener: OnScreenShotListener? = null 
  9.     private var mStartListenTime: Long = 0 
  10.     /** 
  11.      * 内部存储器内容观察者 
  12.      */ 
  13.     private var mInternalObserver: MediaContentObserver? = null 
  14.     /** 
  15.      * 外部存储器内容观察者 
  16.      */ 
  17.     private var mExternalObserver: MediaContentObserver? = null 
  18.     /** 
  19.      * 运行在 UI 线程的 Handler, 用于运行监听器回调 
  20.      */ 
  21.     private var mUiHandler = Handler(Looper.getMainLooper()) 
  22.     init { 
  23.         ScreenShotHelper.showLog("init"
  24.         assertInMainThread() 
  25.         requireNotNull(context) { "The context must not be null." } 
  26.         mContext = context 
  27.         if (mScreenRealSize == null) { 
  28.             mScreenRealSize = getRealScreenSize() 
  29.             if (mScreenRealSize != null) { 
  30.                 ScreenShotHelper.showLog("Screen Real Size: " + mScreenRealSize!!.x + " * " + mScreenRealSize!!.y) 
  31.             } else { 
  32.                 ScreenShotHelper.showLog("Get screen real size failed."
  33.             } 
  34.         } 
  35.     } 
  36.     /** 
  37.      * 单例 
  38.      */ 
  39.     companion object : SingletonHolder<ScreenShotListener, Context>(::ScreenShotListener) 
  40.     /** 
  41.      * 开启监听 
  42.      */ 
  43.     fun startListener() { 
  44.         assertInMainThread() 
  45.         // 记录开始监听的时间戳 
  46.         mStartListenTime = System.currentTimeMillis() 
  47.         // 创建内容观察者 
  48.         mInternalObserver = 
  49.             MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler) 
  50.         mExternalObserver = 
  51.             MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler) 
  52.         // 注册内容观察者 
  53.         mContext.contentResolver.registerContentObserver( 
  54.             MediaStore.Images.Media.INTERNAL_CONTENT_URI, 
  55.             false
  56.             mInternalObserver 
  57.         ) 
  58.         mContext.contentResolver.registerContentObserver( 
  59.             MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
  60.             false
  61.             mExternalObserver 
  62.         ) 
  63.     } 
  64.     fun stopListener() { 
  65.         assertInMainThread() 
  66.         // 注销内容观察者 
  67.         if (mInternalObserver != null) { 
  68.             try { 
  69.                 mContext.contentResolver.unregisterContentObserver(mInternalObserver!!) 
  70.             } catch (e: Exception) { 
  71.                 e.printStackTrace() 
  72.             } 
  73.             mInternalObserver = null 
  74.         } 
  75.         if (mExternalObserver != null) { 
  76.             try { 
  77.                 mContext.contentResolver.unregisterContentObserver(mExternalObserver!!) 
  78.             } catch (e: Exception) { 
  79.                 e.printStackTrace() 
  80.             } 
  81.             mExternalObserver = null 
  82.         } 
  83.         // 清空数据 
  84.         mStartListenTime = 0 
  85.         mListener = null 
  86.     } 
  87.     /** 
  88.      * 处理媒体数据库的内容改变 
  89.      */ 
  90.     fun handleMediaContentChange(contentUri: Uri) { 
  91.         var cursorCursor? = null 
  92.         try { 
  93.             cursor = mContext.contentResolver.query( 
  94.                 contentUri, 
  95.                 if (Build.VERSION.SDK_INT < 16) ScreenShotHelper.MEDIA_PROJECTIONS else ScreenShotHelper.MEDIA_PROJECTIONS_API_16, 
  96.                 nullnull
  97.                 "${MediaStore.Images.ImageColumns.DATE_ADDED} desc limit 1" 
  98.             ) 
  99.             if (cursor == null) { 
  100.                 ScreenShotHelper.showLog("Deviant logic."
  101.                 return 
  102.             } 
  103.             if (!cursor.moveToFirst()) { 
  104.                 ScreenShotHelper.showLog("Cursor no data."
  105.                 return 
  106.             } 
  107.             // 获取各列的索引 
  108.             val dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA) 
  109.             val dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN) 
  110.             var widthIndex = -1 
  111.             var heightIndex = -1 
  112.             if (Build.VERSION.SDK_INT >= 16) { 
  113.                 widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH) 
  114.                 heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT) 
  115.             } 
  116.             // 获取行数据 
  117.             val data = cursor.getString(dataIndex) 
  118.             val dateTaken = cursor.getLong(dateTakenIndex) 
  119.             var width = 0 
  120.             var height = 0 
  121.             if (widthIndex >= 0 && heightIndex >= 0) { 
  122.                 width = cursor.getInt(widthIndex) 
  123.                 height = cursor.getInt(heightIndex) 
  124.             } else { 
  125.                 val size = getImageSize(data) 
  126.                 width = size.x 
  127.                 height = size.y 
  128.             } 
  129.             // 处理获取到的第一行数据 
  130.             handleMediaRowData(data, dateTaken, width, height) 
  131.         } catch (e: Exception) { 
  132.             ScreenShotHelper.showLog("Exception: ${e.message}"
  133.             e.printStackTrace() 
  134.         } finally { 
  135.             if (cursor != null && !cursor.isClosed) { 
  136.                 cursor.close() 
  137.             } 
  138.         } 
  139.     } 
  140.     private fun getImageSize(imagePath: String): Point { 
  141.         val options = BitmapFactory.Options() 
  142.         options.inJustDecodeBounds = true 
  143.         BitmapFactory.decodeFile(imagePath, options) 
  144.         return Point(options.outWidth, options.outHeight) 
  145.     } 
  146.     /** 
  147.      * 处理获取到的一行数据 
  148.      */ 
  149.     private fun handleMediaRowData(data: String, dateTaken: Long, width: Int, height: Int) { 
  150.         if (checkScreenShot(data, dateTaken, width, height)) { 
  151.             ScreenShotHelper.showLog("ScreenShot: path = $data; size = $width * $height; date = $dateTaken"
  152.             if (mListener != null && !checkCallback(data)) { 
  153.                 mListener!!.onScreenShot(data) 
  154.             } 
  155.         } else { 
  156.             // 如果在观察区间媒体数据库有数据改变,又不符合截屏规则,则输出到 log 待分析 
  157.             ScreenShotHelper.showLog("Media content changed, but not screenshot: path = $data; size = $width * $height; date = $dateTaken"
  158.         } 
  159.     } 
  160.     /** 
  161.      * 判断指定的数据行是否符合截屏条件 
  162.      */ 
  163.     private fun checkScreenShot(data: String?, dateTaken: Long, width: Int, height: Int): Boolean { 
  164.         // 判断依据一: 时间判断 
  165.         // 如果加入数据库的时间在开始监听之前, 或者与当前时间相差大于10秒, 则认为当前没有截屏 
  166.         if (dateTaken < mStartListenTime || System.currentTimeMillis() - dateTaken > 10 * 1000) { 
  167.             return false 
  168.         } 
  169.         // 判断依据二: 尺寸判断 
  170.         if (mScreenRealSize != null) { 
  171.             // 如果图片尺寸超出屏幕, 则认为当前没有截屏 
  172.             if (!(width <= mScreenRealSize!!.x && height <= mScreenRealSize!!.y) 
  173.                 || (height <= mScreenRealSize!!.x && width <= mScreenRealSize!!.y) 
  174.             ) { 
  175.                 return false 
  176.             } 
  177.         } 
  178.         // 判断依据三: 路径判断 
  179.         if (data.isNullOrEmpty()) { 
  180.             return false 
  181.         } 
  182.         val lowerData = data.toLowerCase(Locale.getDefault()) 
  183.         // 判断图片路径是否含有指定的关键字之一, 如果有, 则认为当前截屏了 
  184.         for (keyWork in ScreenShotHelper.KEYWORDS) { 
  185.             if (lowerData.contains(keyWork)) { 
  186.                 return true 
  187.             } 
  188.         } 
  189.         return false 
  190.     } 
  191.     /** 
  192.      * 判断是否已回调过, 某些手机ROM截屏一次会发出多次内容改变的通知; <br></br> 
  193.      * 删除一个图片也会发通知, 同时防止删除图片时误将上一张符合截屏规则的图片当做是当前截屏. 
  194.      */ 
  195.     private fun checkCallback(imagePath: String): Boolean { 
  196.         if (mHasCallbackPaths.contains(imagePath)) { 
  197.             ScreenShotHelper.showLog("ScreenShot: imgPath has done; imagePath = $imagePath"
  198.             return true 
  199.         } 
  200.         // 大概缓存15~20条记录便可 
  201.         if (mHasCallbackPaths.size >= 20) { 
  202.             for (i in 0..4) { 
  203.                 mHasCallbackPaths.removeAt(0) 
  204.             } 
  205.         } 
  206.         mHasCallbackPaths.add(imagePath) 
  207.         return false 
  208.     } 
  209.     /** 
  210.      * 获取屏幕分辨率 
  211.      */ 
  212.     private fun getRealScreenSize(): Point? { 
  213.         var screenSize: Point? = null 
  214.         try { 
  215.             screenSize = Point() 
  216.             val windowManager = mContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager 
  217.             val defaultDisplay = windowManager.defaultDisplay 
  218.             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 
  219.                 defaultDisplay.getRealSize(screenSize) 
  220.             } else { 
  221.                 try { 
  222.                     val mGetRawW = Display::class.java.getMethod("getRawWidth"
  223.                     val mGetRawH = Display::class.java.getMethod("getRawHeight"
  224.                     screenSize.set
  225.                         mGetRawW.invoke(defaultDisplay) as Int
  226.                         mGetRawH.invoke(defaultDisplay) as Int 
  227.                     ) 
  228.                 } catch (e: Exception) { 
  229.                     screenSize.set(defaultDisplay.width, defaultDisplay.height) 
  230.                     e.printStackTrace() 
  231.                 } 
  232.             } 
  233.         } catch (e: Exception) { 
  234.             e.printStackTrace() 
  235.         } 
  236.         return screenSize 
  237.     } 
  238.     private fun assertInMainThread() { 
  239.         if (Looper.myLooper() != Looper.getMainLooper()) { 
  240.             val stackTrace = Thread.currentThread().stackTrace 
  241.             var methodMsg: String? = null 
  242.             if (stackTrace != null && stackTrace.size >= 4) { 
  243.                 methodMsg = stackTrace[3].toString() 
  244.             } 
  245.             ScreenShotHelper.showLog("Call the method must be in main thread: $methodMsg"
  246.         } 
  247.     } 
  248.     /** 
  249.      * 媒体内容观察者 
  250.      */ 
  251.     private inner class MediaContentObserver(var contentUri: Uri, handler: Handler) : 
  252.         ContentObserver(handler) { 
  253.         override fun onChange(selfChange: Boolean) { 
  254.             super.onChange(selfChange) 
  255.             handleMediaContentChange(contentUri) 
  256.         } 
  257.     } 
  258.     /** 
  259.      * 设置截屏监听器回调 
  260.      */ 
  261.     fun setListener(listener: OnScreenShotListener) { 
  262.         this.mListener = listener 
  263.     } 
  264.     /** 
  265.      * 截屏监听接口 
  266.      */ 
  267.     interface OnScreenShotListener { 
  268.         fun onScreenShot(picPath: String) 
  269.     } 

3、使用

  1. class ScreenShotActivity : AppCompatActivity() { 
  2.     private lateinit var screenShotListener: ScreenShotListener 
  3.     var isHasScreenShotListener = false 
  4.     override fun onCreate(savedInstanceState: Bundle?) { 
  5.         super.onCreate(savedInstanceState) 
  6.         setContentView(R.layout.activity_screen_shot) 
  7.         screenShotListener = ScreenShotListener.getInstance(this) 
  8.         // 申请权限 
  9.         val permission = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) 
  10.         if (ActivityCompat.checkSelfPermission( 
  11.                 this, 
  12.                 Manifest.permission.READ_EXTERNAL_STORAGE 
  13.             ) != PackageManager.PERMISSION_GRANTED 
  14.         ) { 
  15.             ActivityCompat.requestPermissions(this, permission, 1001) 
  16.         } 
  17.     } 
  18.     override fun onRequestPermissionsResult( 
  19.         requestCode: Int
  20.         permissions: Array<out String>, 
  21.         grantResults: IntArray 
  22.     ) { 
  23.         super.onRequestPermissionsResult(requestCode, permissions, grantResults) 
  24.         if (requestCode == 1001) { 
  25.             if (grantResults[0] == PermissionChecker.PERMISSION_GRANTED) { 
  26.                 customToast("权限申请成功"
  27.             } else { 
  28.                 customToast("权限申请失败"
  29.             } 
  30.         } 
  31.     } 
  32.     override fun onResume() { 
  33.         super.onResume() 
  34.         startScreenShotListen() 
  35.     } 
  36.     override fun onPause() { 
  37.         super.onPause() 
  38.         stopScreenShotListen() 
  39.     } 
  40.     private fun startScreenShotListen() { 
  41.         if (!isHasScreenShotListener) { 
  42.             screenShotListener.setListener(object : ScreenShotListener.OnScreenShotListener { 
  43.                 override fun onScreenShot(picPath: String) { 
  44.                     customToast("监听截屏成功"
  45.                     Log.d(ScreenShotHelper.TAG, picPath) 
  46.                 } 
  47.             }) 
  48.             screenShotListener.startListener() 
  49.             isHasScreenShotListener = true 
  50.         } 
  51.     } 
  52.     private fun stopScreenShotListen() { 
  53.         if (isHasScreenShotListener) { 
  54.             screenShotListener.stopListener() 
  55.             isHasScreenShotListener = false 
  56.         } 
  57.     } 

注意点:

  • 若要监听整个APP的所有页面,则将监听器加入到BaseActivity,在页面的onResume中开启监听,在onPause停止监听;
  • 需要读取内部存储(READ_EXTERNAL_STORAGE)权限;

总结

截屏还有滚动,以后会介绍实现方式;努力进步学习;

本文转载自微信公众号「Android开发编程」

 

责任编辑:姜华 来源: Android开发编程
相关推荐

2020-12-21 16:35:51

JavaScript网页截屏代码

2023-07-25 10:45:48

OHScrcpy鸿蒙

2021-08-05 16:36:16

Windows 11操作系统微软

2021-08-08 14:15:30

Windows 11Windows微软

2011-07-21 15:56:32

iPhone 截屏

2011-07-25 14:44:41

iPhone iPhone开发 截屏

2017-01-11 18:36:04

Android矩形区域截屏移动开发

2009-12-23 14:10:23

Linux截屏工具

2011-05-18 14:24:38

2014-06-16 09:28:08

Linux命令行

2017-03-10 14:23:18

Windows 10Windows截屏

2021-02-11 13:56:21

JSweb插件

2016-01-20 13:08:33

2017-12-11 09:04:53

LinuxScrot截屏

2018-04-24 13:40:59

Python盗号原理截图

2020-02-03 10:10:05

Windows 10技巧Windows

2010-02-24 14:50:33

Ubuntu vim

2018-10-15 10:42:17

Linux截屏工具

2020-02-24 09:45:02

Bash截Linux系统配置

2013-10-24 14:24:17

搜狗输入法
点赞
收藏

51CTO技术栈公众号