Android截屏汇报问题

Android截屏功能实现与优化

背景

在摇一摇实现截屏问题汇报的过程中,截屏有很多中方案,而通过MediaProjection方案是最通用的,于是对相关功能进行封装,提供给业务侧进行使用。

实现

截屏授权

截屏需要弹出系统授权弹框,用单独的Activity进行启动,以后每次使用可唤起此Activity即可。

class HScreenCaptureActivity : ComponentActivity() {

    var projectionManager: MediaProjectionManager? = null
    val screenCaptureLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                //启动前台服务
                startService(Intent(this, HScreenCaptureService::class.java).apply {
                    putExtra("resultCode", result.resultCode)
                    putExtra("data", result.data)
                })
                finish()
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        //启动授权
        projectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager?
        projectionManager?.createScreenCaptureIntent()?.let { screenCaptureLauncher.launch(it) }
    }

    override fun onDestroy() {
        sendBroadcast(Intent(H_BROADCAST_ACTION_SCREEN_CAPTURE))
        super.onDestroy()
    }

    /**
     * 清理缓存
     */
    fun cleanCache() {
        val fileParent = File(filesDir.absolutePath, H_FILE_CAPTURE_PARENT)
        if (fileParent.isDirectory && fileParent.exists()) {
            val fileList = fileParent.listFiles { file -> file.name.endsWith(".png", true) }
            fileList?.forEachIndexed { index, file ->
                file.delete()
            }
        }
    }
}

截屏服务

截屏必须有前台服务和通知栏提醒。在服务中依赖MediaProjection进行屏幕的虚拟映射和截图。在接收到截图后保存图片到本地目录提供后后续业务逻辑使用。

const val H_SCREEN_CAPTURE_CHANNEL_ID = "h_screen_capture"
const val H_FILE_CAPTURE_PARENT = "capture"

const val H_BROADCAST_ACTION_SCREEN_CAPTURE = "H_BROADCAST_ACTION_SCREEN_CAPTURE"
const val H_BROADCAST_ACTION_SCREEN_CAPTURE_RESULT = "H_BROADCAST_ACTION_SCREEN_CAPTURE_RESULT"

class HScreenCaptureService : Service() {

    private var notificationManager: NotificationManager? = null
    private var mProjectionManager: MediaProjectionManager? = null
    private var mMediaProjection: MediaProjection? = null
    private var mVirtualDisplay: VirtualDisplay? = null
    private val VIRTUAL_DISPLAY_NAME: String = "ScreenCapture"
    private var mImageReader: ImageReader? = null

    private val H_CAPTURE_NOTIFICATION_ID by lazy { Random.nextInt() }

    private val mediaProjectionReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val fileParent = File(filesDir.absolutePath, H_FILE_CAPTURE_PARENT)
            if (!fileParent.exists()) {
                fileParent.mkdir()
            }
            val localFile = File(
                fileParent.absolutePath, "${System.currentTimeMillis()}.png"
            )
            saveScreenCaptureMat(localFile)

            sendBroadcast(Intent(H_BROADCAST_ACTION_SCREEN_CAPTURE_RESULT).apply {
                putExtra("image", localFile.absolutePath)
            })
            mMediaProjection?.stop()
            stopForeground(STOP_FOREGROUND_REMOVE)
            stopSelf()
        }
    }

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
        val filter = IntentFilter(H_BROADCAST_ACTION_SCREEN_CAPTURE)
        registerReceiver(mediaProjectionReceiver, filter, RECEIVER_EXPORTED)
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(mediaProjectionReceiver)
    }

    private fun createNotificationChannel() {
        val channel = NotificationChannel(
            H_SCREEN_CAPTURE_CHANNEL_ID, "屏幕捕获服务", NotificationManager.IMPORTANCE_DEFAULT
        )
        notificationManager = getSystemService(NotificationManager::class.java)
        notificationManager?.createNotificationChannel(channel)
    }

    private fun startForegroundService() {
        val notification = NotificationCompat.Builder(this, H_SCREEN_CAPTURE_CHANNEL_ID)
            .setContentTitle("屏幕捕获中").setContentText("应用正在捕获屏幕,您可以在此处查看信息")
            .setSmallIcon(R.drawable.h_icon_checked_icon)
            .setPriority(NotificationCompat.PRIORITY_HIGH).setOngoing(true)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC).build()
        startForeground(H_CAPTURE_NOTIFICATION_ID, notification)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        //FIXME 此处需要兼容低版本
        val resultCode: Int? = intent?.getIntExtra("resultCode", Activity.RESULT_CANCELED)
        val data: Intent = intent?.getParcelableExtra("data", Intent::class.java) as Intent

        startForegroundService()

        mProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager?
        mMediaProjection = mProjectionManager?.getMediaProjection(resultCode ?: 0, data)
        setupVirtualDisplay()

        return START_STICKY
    }

    private fun setupVirtualDisplay() {
        val displayMetrics = resources.displayMetrics
        mImageReader = ImageReader.newInstance(
            displayMetrics.widthPixels, displayMetrics.heightPixels, PixelFormat.RGBA_8888, 2
        )
        mMediaProjection?.registerCallback(object : MediaProjection.Callback() {}, null)
        mVirtualDisplay = mMediaProjection?.createVirtualDisplay(
            VIRTUAL_DISPLAY_NAME,
            displayMetrics.widthPixels,
            displayMetrics.heightPixels,
            displayMetrics.densityDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            mImageReader?.surface,
            null,
            null
        )
    }

    fun saveScreenCaptureMat(localFile: File) {
        try {
            val image = mImageReader?.acquireLatestImage()
            if (image != null) {
                val planes = image.planes
                val buffer = planes[0].buffer
                val pixelStride = planes[0].pixelStride
                val rowStride = planes[0].rowStride
                val rowPadding = rowStride - pixelStride * (mImageReader?.width ?: 0)

                val bitmap: Bitmap = Bitmap.createBitmap(
                    (mImageReader?.width ?: 0) + rowPadding / pixelStride,
                    mImageReader?.height ?: 0,
                    Bitmap.Config.ARGB_8888
                )
                bitmap.copyPixelsFromBuffer(buffer)

                //写文件
                val os = FileOutputStream(localFile)
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
                os.flush()
                bitmap.recycle()
            }
            image?.close()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun onBind(p0: Intent?): IBinder? {
        return null
    }
}

截屏处理服务记得在清单文件进行声明,特别主要一定要添加android:foregroundServiceType="mediaProjection"。

<service
    android:name="com.zhb.devkit.service.HScreenCaptureService"
    android:exported="true"
    android:foregroundServiceType="mediaProjection" />

截屏业务处理

在需要接收的地方进行广播注册和业务处理。

registerReceiver(
    object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val localFile: String? = intent?.getStringExtra("image")
            startActivity(Intent(this@MainActivity, FeedbackActivity::class.java).apply {
                putExtra(Constants.PARAMS_KEY_DATA, localFile)
            })
        }
    }, IntentFilter(H_BROADCAST_ACTION_SCREEN_CAPTURE_RESULT), RECEIVER_EXPORTED
)

评价

整体效果可以达到预期,使用顺滑无BUG。在测试过程中,发现对低版本设备不兼容,后续会进行低版本机型的适配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泓博

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值