背景
在摇一摇实现截屏问题汇报的过程中,截屏有很多中方案,而通过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。在测试过程中,发现对低版本设备不兼容,后续会进行低版本机型的适配。