Como implementar a captura de tela do Android

I. Introdução

Existe uma função de captura de tela na obra, mas o problema de não conseguir capturar a caixa de diálogo ou o problema do WebView ocorrerá pela forma de obtenção da Janela, então aqui utilizamos a API de captura de tela que surgiu após o 5.0 para fazer isto. É principalmente para entrar no programa para inicialização (deve-se notar que o intervalo de tempo direto entre a inicialização e a captura de tela não deve ser inferior a 5s, caso contrário, haverá capturas de tela antes que a inicialização seja concluída, o que levará à falha). . A desvantagem é que, por conveniência e economia de problemas, o método aqui é obter o conteúdo da tela com um atraso, em vez de aguardar a operação de retorno após a conclusão do armazenamento em buffer. No entanto, as soluções relacionadas referem-se ao link abaixo para modificação.

  1. Android implementa método de captura de tela (resumo)
  2. Várias maneiras de realizar a captura de tela do Android

Nota: A captura de tela do View é obtida através da janela Window, mas a caixa de diálogo e similares não podem ser obtidas, portanto, a imagem final obtida não os inclui.

Consulte o seguinte método para usar o método de captura de tela do sistema:

  1. O Android alcança capturas de tela globais e gravação de tela
  2. Aplicativo Android Camera2 (04) gravando e salvando o processo de vídeo
  3. Captura de tela do Android 5.0 Image image = imageReader.acquireNextImage() relatar solução de ponteiro nulo
  4. Uso de notificação do Android

Onde as seguintes funções precisam ser otimizadas:

  1. a. O serviço não pode ser colocado em ViewModel, caso contrário, haverá possíveis vazamentos de memória.
    O site oficial explica isso:
    consulte o site oficial aqui
    b. A explicação para esse problema é stackOverflow

Nota: O ViewModel nunca deve se referir à View, Lifecycle ou qualquer classe que possa armazenar uma referência ao contexto Activity.

2. Códigos relacionados

O código geral é colocado no ViewModel para operar
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);
        }
    }
}

Como usar
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()
               }
            }
        }
    }
}

3. Link de referência

  1. Converter a função de retorno de chamada para Flow

Acho que você gosta

Origin blog.csdn.net/Mr_Tony/article/details/126454494
Recomendado
Clasificación