Android截屏的方式

4,522 阅读3分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

前言

今天我们来介绍下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)权限;

总结

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