How to implement Android screenshot

I. Introduction

There is a screen capture function in the work, but the problem of not being able to capture the dialog box or the WebView problem will occur through the way of obtaining the Window, so here we use the screen capture API that appeared after 5.0 to do it. It is mainly to enter the program for initialization (it should be noted that the direct time interval between initialization and screenshot should not be less than 5s, otherwise there will be screenshots before the initialization is completed, which will lead to failure). resource operations. The disadvantage is that for convenience and trouble-saving, the method here is to obtain the screen content with a delay instead of waiting for the return operation after the buffering is completed. However, related solutions refer to the link below for modification.

  1. Android implements screen capture method (summary)
  2. Several ways to realize android screen capture

Note: The screenshot of the View is obtained through the Window window, but the dialog box and the like cannot be obtained, so the final obtained picture does not include these.

Refer to the following method to use the system screenshot method:

  1. Android achieves global screenshots and screen recording
  2. Android APP Camera2 application (04) recording & saving video process
  3. Android 5.0 screenshot Image image = imageReader.acquireNextImage() report null pointer solution
  4. Android Notification use

Where the following functions need to be optimized:

  1. a. Service cannot be placed in ViewModel, otherwise there will be potential memory leaks.
    The official website explains this:
    refer to the official website here
    b. The explanation for this problem is stackOverflow

Note: The ViewModel must never refer to the View, Lifecycle, or any class that might store a reference to the Activity context.

2. Related codes

The overall code is placed in the ViewModel to operate
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);
        }
    }
}

How to use
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. Reference link

  1. Convert the callback function to Flow

Guess you like

Origin blog.csdn.net/Mr_Tony/article/details/126454494