一、前言
工作中有截屏功能,但是通过获取Window的方式会出现无法截取对话框的问题,或者WebView的问题,因此这里采取使用5.0之后出现的截屏api来做。主要是进入程序进行初始化(需要注意的是,初始化和截屏直接时间间隔不要低于5s,否则会出现初始化未完成就去截屏,会导致失败),截屏之后及时关闭资源,后面再次开启截屏时候再次开启资源等操作。不足之处是为了方便省事,这里采取的是延时获取屏幕内容而不是通过缓冲完毕等回掉操作。
注意:通过Window窗口获取View的截屏,获取不到对话框之类的东西,所以最终获取的图片是不包含这些的。
参考下面的方式使用系统截屏的方法:
以下功能待优化的地方:
- a、 Service不可以放在ViewModel里面否则会出现潜在的内存泄漏
官网对此的解释是:
参考此处官网 b、对该问题的解释为stackOverflow
注意:ViewModel 绝不能引用视图、Lifecycle 或可能存储对 Activity 上下文的引用的任何类。
二、相关代码
整体代码是放在ViewModel里面去操作的build.gradle
android {
compileSdkVersion 33
defaultConfig {
minSdk 21
targetSdk 33
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.1"
implementation "androidx.core:core-ktx:1.8.0"
implementation 'androidx.media:media:1.6.0' //关键是这个,其它的版本保持最新即可,这个不写的话会使用androidSdk中默认的那个,那个版本比较低,会出问题
}
AndroidManifest.xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<service android:name="service.CaptureService"
android:enabled="true"
android:foregroundServiceType="mediaProjection"/>
CaptureService.kt
//截屏的服务
class CaptureService: Service() {
var capturor: ScreenCapture?= null
inner class CaptureServiceBinder: Binder(){
fun getCaptureService(): CaptureService{
return this@CaptureService
}
}
override fun onBind(intent: Intent?): IBinder {
Log.e("YM---->CaptureService","onBind")
return CaptureServiceBinder()
}
private var mResultCode = -1
private var mResultData = Intent()
private val screenSize by lazy {
loadScreenSize()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.e("YM---->CaptureService","onStartCommand")
mResultCode = intent?.getIntExtra("code", -1) ?: -1
mResultData = intent?.getParcelableExtra("data") ?: Intent()
initForegroundCapture()
initScreenCapture()
return super.onStartCommand(intent, flags, startId)
}
override fun stopService(name: Intent?): Boolean {
Log.e("YM---->CaptureService","stopService")
return super.stopService(name)
}
override fun unbindService(conn: ServiceConnection) {
super.unbindService(conn)
Log.e("YM---->CaptureService","unbindService")
}
override fun onUnbind(intent: Intent?): Boolean {
Log.e("YM---->CaptureService","onUnbind")
return super.onUnbind(intent)
}
private fun initScreenCapture(){
Log.e("YM--->","--->初始化截屏")
val mProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
capturor = ScreenCapture(screenSize.first, screenSize.second,mProjectionManager)
capturor?.initCapture(mResultCode,mResultData)
}
fun startCapture(completable :CompletableDeferred<Bitmap?>){
capturor?.startCapture(completable)
}
fun release(){
capturor?.release()
}
private fun initForegroundCapture(){
val mBuilder: NotificationCompat.Builder =NotificationCompat.
Builder(applicationContext).setAutoCancel(true) // 点击后让通知将消失
mBuilder.setContentText("抓屏服务运行中")
mBuilder.setContentTitle(resources.getString(R.string.app_name))
mBuilder.setSmallIcon(R.drawable.ic_launcher)
mBuilder.setWhen(System.currentTimeMillis())
mBuilder.priority = Notification.PRIORITY_DEFAULT //设置该通知优先级
mBuilder.setOngoing(false) //ture,设置他为一个正在进行的通知。
mBuilder.setDefaults(Notification.DEFAULT_ALL)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val channelId = "channelId" + System.currentTimeMillis()
val channel = NotificationChannel(
channelId,
resources.getString(R.string.app_name),
NotificationManager.IMPORTANCE_HIGH
)
manager.createNotificationChannel(channel)
mBuilder.setChannelId(channelId)
}
mBuilder.setContentIntent(null)
startForeground(11, mBuilder.build())
}
private fun loadScreenSize(): Pair<Int,Int>{
val wm = this.getSystemService(WINDOW_SERVICE) as WindowManager
val width = wm.defaultDisplay.width
val height = wm.defaultDisplay.height
return width to height
}
}
ScreenCapture,kt
//截屏工具类
class ScreenCapture constructor(private val width : Int ,private val height : Int,mProjectionManager: MediaProjectionManager){
private var mImageReader : ImageReader ?= null
private var mediaProjectionManager = mProjectionManager
private var mediaProjection: MediaProjection ?= null
private var virtual: VirtualDisplay ?= null
//初始化截图功能
fun initCapture(resultCode : Int, data : Intent) {
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data)
}
//开始截屏
fun startCapture(completable :CompletableDeferred<Bitmap?>){
mImageReader = ImageReader.newInstance(width,height, PixelFormat.RGBA_8888,3)
virtual = mediaProjection?.createVirtualDisplay("capture",width,height,1,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
mImageReader!!.surface,null,null)
// setOnImageAvailableListener里面有两个参数,一个是可以获取图像的监听,一个是切换线程的handle,handler如果不传,则表示不切换线程
// 为了便于处理逻辑,该函数整体由异步初始化,利用kotlin特性将其转换为结构式异步编程,如果想切换线程可以参考注释掉的handler代码
// //在后台线程里保存文件
// var backgroundHandler: Handler? = null
//
// @JvmName("getBackgroundHandler1")
// private fun getBackgroundHandler(): Handler? {
// if (backgroundHandler == null) {
// val backgroundThread = HandlerThread("catwindow", Process.THREAD_PRIORITY_BACKGROUND)
// backgroundThread.start()
// backgroundHandler = Handler(backgroundThread.looper)
// }
// return backgroundHandler
// }
mImageReader?.setOnImageAvailableListener({
val bitmap = acquire()//保存图像
completable.complete(bitmap)
},null)//这个Handler是用来表示其回调是在哪个线程进行,传null的话表示不切换线程。
completable.invokeOnCompletion {
if (completable.isCancelled) {
Log.e("YM--->","任务已经撤销")
}
}
}
fun release(){
mImageReader?.close()
virtual?.release()
}
private fun acquire() : Bitmap?{
var image: Image? = null
//当未开始录制的时候先调用此方法会报错
//java.lang.IllegalStateException: mImageReader.acquireLatestImage() must not be null
try {
image = mImageReader?.acquireLatestImage()
if (null == image) return null
//此高度和宽度似乎与ImageReader构造方法中的高和宽一致
val iWidth = image.width
val iHeight = image.height
//panles的数量与图片的格式有关
val plane = image.planes[0]
val bytebuffer = plane.buffer
//计算偏移量
val pixelStride = plane.pixelStride
val rowStride = plane.rowStride;
val rowPadding = rowStride - pixelStride * iWidth;
val bitmap = Bitmap.createBitmap(iWidth + rowPadding / pixelStride,
iHeight, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(bytebuffer)
//必须要有这一步,不如图片会有黑边
return Bitmap.createBitmap(bitmap,0,0,iWidth,iHeight)
}catch (e : Exception){
e.printStackTrace()
return null
}finally {
image?.close()
}
}
}
CaptureViewModel.kt
class CaptureViewModel(val context: Application) : AndroidViewModel(context) {
private var captureService: CaptureService? = null//这一行会有内存泄露,暂时没解决
//服务链接
private val captureServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as CaptureService.CaptureServiceBinder
captureService = binder.getCaptureService()
}
override fun onServiceDisconnected(name: ComponentName?) {
captureService = null
}
}
fun loadScreenCaptureService(resultCode: Int, data: Intent) {
val captureService = Intent(context, CaptureService::class.java)
captureService.putExtra("code", resultCode)
captureService.putExtra("data", data)
context.bindService(
captureService,
captureServiceConnection,
AppCompatActivity.BIND_AUTO_CREATE
)
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
//适配8.0机制
context.startForegroundService(captureService)
} else {
context.startService(captureService);
}
}
//服务启动前十秒不可以使用该功能
fun screenshot() {
viewModelScope.launch(Dispatchers.IO) {
val completableCapture = CompletableDeferred<Bitmap?>()
captureService?.startCapture(completableCapture)
val bitmap = completableCapture.await()
captureService?.release()
completableCapture.cancel()//昨晚之后进行关闭操作
//这里获取了bitmap,可以做别的操作
}
}
override fun onCleared() {
super.onCleared()
context.unbindService(captureServiceConnection)
val service = Intent(context, CaptureService::class.java)
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
//适配8.0机制
context.stopService(service)
} else {
context.stopService(service);
}
}
}
使用方式MainActivity.kt
class MainActivity : AppCompatActivity(){
private val REQUEST_MEDIA_PROJECTION = 10
private val viewModel: CaptureViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
initCapturePermission()
}
//初始化截屏权限
private fun initCapturePermission() {
val mProjectionManager =
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
//启动MediaProjection并准备截图
startActivityForResult(
mProjectionManager.createScreenCaptureIntent(),
REQUEST_MEDIA_PROJECTION
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.e("YM--->onActivityResult", "--->requestCode:$requestCode --->resultCode:$resultCode")
if (requestCode == REQUEST_MEDIA_PROJECTION) {
if (resultCode != Activity.RESULT_OK) {
Toast.makeText(this, "截屏权限被拒绝,将无法使用抓屏功能!", Toast.LENGTH_SHORT).show()
} else {
if (null == data) return
viewModel.loadScreenCaptureService(resultCode, data)
lifecycleScope.launch {
delay(5000)//大约延迟5秒时间
viewModel.screenshot()
}
}
}
}
}