Android截屏的实现方式

一、前言

工作中有截屏功能,但是通过获取Window的方式会出现无法截取对话框的问题,或者WebView的问题,因此这里采取使用5.0之后出现的截屏api来做。主要是进入程序进行初始化(需要注意的是,初始化和截屏直接时间间隔不要低于5s,否则会出现初始化未完成就去截屏,会导致失败),截屏之后及时关闭资源,后面再次开启截屏时候再次开启资源等操作。不足之处是为了方便省事,这里采取的是延时获取屏幕内容而不是通过缓冲完毕等回掉操作。不过相关解决方式参考下文的链接进行修改。

  1. Android实现截屏方式整理(总结)
  2. android 截屏实现的几种方式

注意:通过Window窗口获取View的截屏,获取不到对话框之类的东西,所以最终获取的图片是不包含这些的。

参考下面的方式使用系统截屏的方法:

  1. ANDROID实现全局截图以及录屏
  2. Android APP Camera2应用(04)录像&保存视频流程
  3. Android 5.0 截屏 Image image = imageReader.acquireNextImage()报空指针解决方案
  4. Android Notification使用

以下功能待优化的地方:

  1. 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()
               }
            }
        }
    }
}

三、参考链接

  1. 将回调函数转为Flow
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值