截屏實現方式和監聽截屏詳解
前言
今天我們來介紹下Android里截屏方面的知識點介紹;
一、Android截屏的方式
1、獲取DecorView截屏
通過獲取DecorView的方式來實現截屏(前提是當前Activity已經加載完成),DecorView為整個Window界面的最頂層View,因此截屏不包含狀態欄(SystemUI)部分.
方式一
- View view = getWindow().getDecorView(); // 獲取DecorView
- view.setDrawingCacheEnabled(true);
- view.buildDrawingCache();
- Bitmap bitmap1 = view.getDrawingCache();
方式二
- Bitmap bitmap2 = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas();
- canvas.setBitmap(bitmap2);
- view.draw(canvas);
保存 bitmap1 或 bitmap2 均可保存截屏圖片
2、調用系統源碼截屏
由于是hide api,通過反射調用如下:
- public Bitmap takeScreenShot() {
- Bitmap bmp = null;
- mDisplay.getMetrics(mDisplayMetrics);
- float[] dims = {(float) mDisplayMetrics.widthPixels, (float) heightPixels};
- float degrees = getDegreesForRotation(mDisplay.getRotation());
- boolean requiresRotation = degrees > 0;
- if (requiresRotation) {
- mDisplayMatrix.reset();
- mDisplayMatrix.preRotate(-degrees);
- mDisplayMatrix.mapPoints(dims);
- dims[0] = Math.abs(dims[0]);
- dims[1] = Math.abs(dims[1]);
- }
- try {
- Class<?> demo = Class.forName("android.view.SurfaceControl");
- Method method = demo.getMethod("screenshot", new Class[]{Integer.TYPE, Integer.TYPE});
- bmp = (Bitmap) method.invoke(demo, new Object[]{Integer.valueOf((int) dims[0]), Integer.valueOf((int) dims[1])});
- if (bmp == null) {
- return null;
- }
- if (requiresRotation) {
- Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels, heightPixels, Bitmap.Config.RGB_565);
- Canvas c = new Canvas(ss);
- c.translate((float) (ss.getWidth() / 2), (float) (ss.getHeight() / 2));
- c.rotate(degrees);
- c.translate((-dims[0] / 2), (-dims[1] / 2));
- c.drawBitmap(bmp, 0, 0, null);
- c.setBitmap(null);
- bmp.recycle();
- bmp = ss;
- }
- if (bmp == null) {
- return null;
- }
- bmp.setHasAlpha(false);
- bmp.prepareToDraw();
- return bmp;
- } catch (Exception e) {
- e.printStackTrace();
- return bmp;
- }
- }
二、截屏手機里的監聽
利用FileObserver監聽某個目錄中資源變化情況;
利用ContentObserver監聽全部資源的變化;
ContentObserver:通過ContentObserver監聽圖片多媒體的變化,當手機上有新圖片文件產生時會通過MediaProvider類向圖片數據庫插入一條記錄,監聽圖片插入事件來獲得圖片的URI;
今天講的是通過ContentObserver實現;
1、ScreenShotHelper截屏幫助類
- /**
- * Description: 截屏幫助類
- */
- class ScreenShotHelper {
- companion object {
- const val TAG = "ScreenShotLog"
- /**
- * 讀取媒體數據庫時需要讀取的列
- */
- val MEDIA_PROJECTIONS = arrayOf(
- MediaStore.Images.ImageColumns.DATA,
- MediaStore.Images.ImageColumns.DATE_TAKEN
- )
- /**
- * 讀取媒體數據庫時需要讀取的列,其中 width、height 字段在 API 16 之后才有
- */
- val MEDIA_PROJECTIONS_API_16 = arrayOf(
- MediaStore.Images.ImageColumns.DATA,
- MediaStore.Images.ImageColumns.DATE_TAKEN,
- MediaStore.Images.ImageColumns.WIDTH,
- MediaStore.Images.ImageColumns.HEIGHT
- )
- /**
- * 截屏路徑判斷的關鍵字
- */
- val KEYWORDS = arrayOf(
- "screenshot", "screen_shot", "screen-shot", "screen shot",
- "screencapture", "screen_capture", "screen-capture", "screen capture",
- "screencap", "screen_cap", "screen-cap", "screen cap"
- )
- fun showLog(msg: String) {
- Log.d(TAG, msg)
- }
- }
- }
2、監聽器ScreenShotListener
- /**
- * Description: 截屏監聽
- */
- class ScreenShotListener constructor(context: Context?) {
- private var mContext: Context
- private var mScreenRealSize: Point? = null
- private val mHasCallbackPaths: ArrayList<String> = ArrayList()
- private var mListener: OnScreenShotListener? = null
- private var mStartListenTime: Long = 0
- /**
- * 內部存儲器內容觀察者
- */
- private var mInternalObserver: MediaContentObserver? = null
- /**
- * 外部存儲器內容觀察者
- */
- private var mExternalObserver: MediaContentObserver? = null
- /**
- * 運行在 UI 線程的 Handler, 用于運行監聽器回調
- */
- private var mUiHandler = Handler(Looper.getMainLooper())
- init {
- ScreenShotHelper.showLog("init")
- assertInMainThread()
- requireNotNull(context) { "The context must not be null." }
- mContext = context
- if (mScreenRealSize == null) {
- mScreenRealSize = getRealScreenSize()
- if (mScreenRealSize != null) {
- ScreenShotHelper.showLog("Screen Real Size: " + mScreenRealSize!!.x + " * " + mScreenRealSize!!.y)
- } else {
- ScreenShotHelper.showLog("Get screen real size failed.")
- }
- }
- }
- /**
- * 單例
- */
- companion object : SingletonHolder<ScreenShotListener, Context>(::ScreenShotListener)
- /**
- * 開啟監聽
- */
- fun startListener() {
- assertInMainThread()
- // 記錄開始監聽的時間戳
- mStartListenTime = System.currentTimeMillis()
- // 創建內容觀察者
- mInternalObserver =
- MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler)
- mExternalObserver =
- MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler)
- // 注冊內容觀察者
- mContext.contentResolver.registerContentObserver(
- MediaStore.Images.Media.INTERNAL_CONTENT_URI,
- false,
- mInternalObserver
- )
- mContext.contentResolver.registerContentObserver(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- false,
- mExternalObserver
- )
- }
- fun stopListener() {
- assertInMainThread()
- // 注銷內容觀察者
- if (mInternalObserver != null) {
- try {
- mContext.contentResolver.unregisterContentObserver(mInternalObserver!!)
- } catch (e: Exception) {
- e.printStackTrace()
- }
- mInternalObserver = null
- }
- if (mExternalObserver != null) {
- try {
- mContext.contentResolver.unregisterContentObserver(mExternalObserver!!)
- } catch (e: Exception) {
- e.printStackTrace()
- }
- mExternalObserver = null
- }
- // 清空數據
- mStartListenTime = 0
- mListener = null
- }
- /**
- * 處理媒體數據庫的內容改變
- */
- fun handleMediaContentChange(contentUri: Uri) {
- var cursor: Cursor? = null
- try {
- cursor = mContext.contentResolver.query(
- contentUri,
- if (Build.VERSION.SDK_INT < 16) ScreenShotHelper.MEDIA_PROJECTIONS else ScreenShotHelper.MEDIA_PROJECTIONS_API_16,
- null, null,
- "${MediaStore.Images.ImageColumns.DATE_ADDED} desc limit 1"
- )
- if (cursor == null) {
- ScreenShotHelper.showLog("Deviant logic.")
- return
- }
- if (!cursor.moveToFirst()) {
- ScreenShotHelper.showLog("Cursor no data.")
- return
- }
- // 獲取各列的索引
- val dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)
- val dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN)
- var widthIndex = -1
- var heightIndex = -1
- if (Build.VERSION.SDK_INT >= 16) {
- widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH)
- heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT)
- }
- // 獲取行數據
- val data = cursor.getString(dataIndex)
- val dateTaken = cursor.getLong(dateTakenIndex)
- var width = 0
- var height = 0
- if (widthIndex >= 0 && heightIndex >= 0) {
- width = cursor.getInt(widthIndex)
- height = cursor.getInt(heightIndex)
- } else {
- val size = getImageSize(data)
- width = size.x
- height = size.y
- }
- // 處理獲取到的第一行數據
- handleMediaRowData(data, dateTaken, width, height)
- } catch (e: Exception) {
- ScreenShotHelper.showLog("Exception: ${e.message}")
- e.printStackTrace()
- } finally {
- if (cursor != null && !cursor.isClosed) {
- cursor.close()
- }
- }
- }
- private fun getImageSize(imagePath: String): Point {
- val options = BitmapFactory.Options()
- options.inJustDecodeBounds = true
- BitmapFactory.decodeFile(imagePath, options)
- return Point(options.outWidth, options.outHeight)
- }
- /**
- * 處理獲取到的一行數據
- */
- private fun handleMediaRowData(data: String, dateTaken: Long, width: Int, height: Int) {
- if (checkScreenShot(data, dateTaken, width, height)) {
- ScreenShotHelper.showLog("ScreenShot: path = $data; size = $width * $height; date = $dateTaken")
- if (mListener != null && !checkCallback(data)) {
- mListener!!.onScreenShot(data)
- }
- } else {
- // 如果在觀察區間媒體數據庫有數據改變,又不符合截屏規則,則輸出到 log 待分析
- ScreenShotHelper.showLog("Media content changed, but not screenshot: path = $data; size = $width * $height; date = $dateTaken")
- }
- }
- /**
- * 判斷指定的數據行是否符合截屏條件
- */
- private fun checkScreenShot(data: String?, dateTaken: Long, width: Int, height: Int): Boolean {
- // 判斷依據一: 時間判斷
- // 如果加入數據庫的時間在開始監聽之前, 或者與當前時間相差大于10秒, 則認為當前沒有截屏
- if (dateTaken < mStartListenTime || System.currentTimeMillis() - dateTaken > 10 * 1000) {
- return false
- }
- // 判斷依據二: 尺寸判斷
- if (mScreenRealSize != null) {
- // 如果圖片尺寸超出屏幕, 則認為當前沒有截屏
- if (!(width <= mScreenRealSize!!.x && height <= mScreenRealSize!!.y)
- || (height <= mScreenRealSize!!.x && width <= mScreenRealSize!!.y)
- ) {
- return false
- }
- }
- // 判斷依據三: 路徑判斷
- if (data.isNullOrEmpty()) {
- return false
- }
- val lowerData = data.toLowerCase(Locale.getDefault())
- // 判斷圖片路徑是否含有指定的關鍵字之一, 如果有, 則認為當前截屏了
- for (keyWork in ScreenShotHelper.KEYWORDS) {
- if (lowerData.contains(keyWork)) {
- return true
- }
- }
- return false
- }
- /**
- * 判斷是否已回調過, 某些手機ROM截屏一次會發出多次內容改變的通知; <br></br>
- * 刪除一個圖片也會發通知, 同時防止刪除圖片時誤將上一張符合截屏規則的圖片當做是當前截屏.
- */
- private fun checkCallback(imagePath: String): Boolean {
- if (mHasCallbackPaths.contains(imagePath)) {
- ScreenShotHelper.showLog("ScreenShot: imgPath has done; imagePath = $imagePath")
- return true
- }
- // 大概緩存15~20條記錄便可
- if (mHasCallbackPaths.size >= 20) {
- for (i in 0..4) {
- mHasCallbackPaths.removeAt(0)
- }
- }
- mHasCallbackPaths.add(imagePath)
- return false
- }
- /**
- * 獲取屏幕分辨率
- */
- private fun getRealScreenSize(): Point? {
- var screenSize: Point? = null
- try {
- screenSize = Point()
- val windowManager = mContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
- val defaultDisplay = windowManager.defaultDisplay
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- defaultDisplay.getRealSize(screenSize)
- } else {
- try {
- val mGetRawW = Display::class.java.getMethod("getRawWidth")
- val mGetRawH = Display::class.java.getMethod("getRawHeight")
- screenSize.set(
- mGetRawW.invoke(defaultDisplay) as Int,
- mGetRawH.invoke(defaultDisplay) as Int
- )
- } catch (e: Exception) {
- screenSize.set(defaultDisplay.width, defaultDisplay.height)
- e.printStackTrace()
- }
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
- return screenSize
- }
- private fun assertInMainThread() {
- if (Looper.myLooper() != Looper.getMainLooper()) {
- val stackTrace = Thread.currentThread().stackTrace
- var methodMsg: String? = null
- if (stackTrace != null && stackTrace.size >= 4) {
- methodMsg = stackTrace[3].toString()
- }
- ScreenShotHelper.showLog("Call the method must be in main thread: $methodMsg")
- }
- }
- /**
- * 媒體內容觀察者
- */
- private inner class MediaContentObserver(var contentUri: Uri, handler: Handler) :
- ContentObserver(handler) {
- override fun onChange(selfChange: Boolean) {
- super.onChange(selfChange)
- handleMediaContentChange(contentUri)
- }
- }
- /**
- * 設置截屏監聽器回調
- */
- fun setListener(listener: OnScreenShotListener) {
- this.mListener = listener
- }
- /**
- * 截屏監聽接口
- */
- interface OnScreenShotListener {
- fun onScreenShot(picPath: String)
- }
- }
3、使用
- class ScreenShotActivity : AppCompatActivity() {
- private lateinit var screenShotListener: ScreenShotListener
- var isHasScreenShotListener = false
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_screen_shot)
- screenShotListener = ScreenShotListener.getInstance(this)
- // 申請權限
- val permission = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
- if (ActivityCompat.checkSelfPermission(
- this,
- Manifest.permission.READ_EXTERNAL_STORAGE
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- ActivityCompat.requestPermissions(this, permission, 1001)
- }
- }
- override fun onRequestPermissionsResult(
- requestCode: Int,
- permissions: Array<out String>,
- grantResults: IntArray
- ) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if (requestCode == 1001) {
- if (grantResults[0] == PermissionChecker.PERMISSION_GRANTED) {
- customToast("權限申請成功")
- } else {
- customToast("權限申請失敗")
- }
- }
- }
- override fun onResume() {
- super.onResume()
- startScreenShotListen()
- }
- override fun onPause() {
- super.onPause()
- stopScreenShotListen()
- }
- private fun startScreenShotListen() {
- if (!isHasScreenShotListener) {
- screenShotListener.setListener(object : ScreenShotListener.OnScreenShotListener {
- override fun onScreenShot(picPath: String) {
- customToast("監聽截屏成功")
- Log.d(ScreenShotHelper.TAG, picPath)
- }
- })
- screenShotListener.startListener()
- isHasScreenShotListener = true
- }
- }
- private fun stopScreenShotListen() {
- if (isHasScreenShotListener) {
- screenShotListener.stopListener()
- isHasScreenShotListener = false
- }
- }
- }
注意點:
- 若要監聽整個APP的所有頁面,則將監聽器加入到BaseActivity,在頁面的onResume中開啟監聽,在onPause停止監聽;
- 需要讀取內部存儲(READ_EXTERNAL_STORAGE)權限;
總結
截屏還有滾動,以后會介紹實現方式;努力進步學習;
本文轉載自微信公眾號「Android開發編程」