Android中如何捕获含GameActivity OpenGL内容的全屏Bitmap?
问题原因
View.draw() 方法仅能捕获Android View体系内的绘制内容,而Jetpack GameActivity的游戏画面是通过OpenGL ES渲染到SurfaceView/TextureView的,这部分内容并不在View的绘制流程中,因此调用rootView.draw(c)只会绘制原生覆盖视图,游戏画面无法被捕获。
方案1:使用MediaProjection API(系统级屏幕捕获)
这是最可靠的方案,能捕获整个屏幕的所有内容(包括OpenGL游戏画面和原生视图),仅需申请屏幕捕获权限。
步骤
- 申请MediaProjection权限
- 创建VirtualDisplay,将屏幕内容输出到Surface
- 从Surface中获取帧数据并转换为Bitmap
代码示例
private var mediaProjection: MediaProjection? = null private var imageReader: ImageReader? = null // 初始化MediaProjection并申请权限 private fun initMediaProjection() { val captureIntent = MediaProjectionManager.getSingleton(this).createScreenCaptureIntent() registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK && result.data != null) { mediaProjection = MediaProjectionManager.getSingleton(this) .getMediaProjection(result.resultCode, result.data!!) setupVirtualDisplay() } }.launch(captureIntent) } // 设置VirtualDisplay用于接收屏幕帧 private fun setupVirtualDisplay() { val displayMetrics = resources.displayMetrics val width = displayMetrics.widthPixels val height = displayMetrics.heightPixels val density = displayMetrics.densityDpi imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2) mediaProjection?.createVirtualDisplay( "ScreenCapture", width, height, density, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader?.surface, null, null ) } // 捕获并转换屏幕内容为Bitmap private fun captureScreen(): Bitmap? { val image = imageReader?.acquireLatestImage() ?: return null val planes = image.planes val buffer = planes[0].buffer val pixelStride = planes[0].pixelStride val rowStride = planes[0].rowStride val rowPadding = rowStride - pixelStride * image.width val bitmap = Bitmap.createBitmap( image.width + rowPadding / pixelStride, image.height, Bitmap.Config.ARGB_8888 ) bitmap.copyPixelsFromBuffer(buffer) image.close() // 裁剪掉多余的padding区域 return Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height) } // 页面销毁时释放资源 override fun onDestroy() { super.onDestroy() mediaProjection?.stop() imageReader?.close() }
方案2:结合OpenGL帧读取+原生视图绘制
如果你能控制游戏的OpenGL渲染流程,可以在游戏渲染完一帧后,读取OpenGL帧数据到Bitmap,再将原生覆盖视图绘制到这个Bitmap上。
代码示例
// 在GL线程中读取当前OpenGL帧数据 private fun readGLFrame(width: Int, height: Int): Bitmap { val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val pixelBuffer = IntArray(width * height) // 读取OpenGL帧(必须在GL线程调用) GLES20.glReadPixels( 0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuffer ) // OpenGL坐标系Y轴向上,需要翻转图像适配Android坐标系 for (y in 0 until height) { for (x in 0 until width) { bitmap.setPixel(x, height - y - 1, pixelBuffer[y * width + x]) } } return bitmap } // 在主线程将覆盖视图绘制到GL帧Bitmap上 private fun combineGLFrameAndOverlay(glBitmap: Bitmap): Bitmap { val combinedBitmap = glBitmap.copy(Bitmap.Config.ARGB_8888, true) val canvas = Canvas(combinedBitmap) // 绘制原生覆盖视图 overlayViews.draw(canvas) return combinedBitmap } // 使用示例:跨线程完成帧读取与视图合并 fun captureGameWithOverlay() { val gameView = findViewById<View>(R.id.game_surface_view) // 替换为你的游戏视图ID val glWidth = gameView.width val glHeight = gameView.height // 在GL线程读取帧数据 gameView.queueEvent { val glBitmap = readGLFrame(glWidth, glHeight) // 切换到主线程合并视图 runOnUiThread { val finalBitmap = combineGLFrameAndOverlay(glBitmap) // 保存或使用finalBitmap } } }
注意事项
- 方案1需要用户授权屏幕捕获,无需修改游戏逻辑,适合大多数通用场景。
- 方案2需要游戏端配合,适合对捕获性能要求较高或无法使用MediaProjection的场景,需注意GL线程与主线程的切换,避免ANR。
内容的提问来源于stack exchange,提问作者Felipe Ribeiro R. Magalhaes
相关产品推荐
相关产品推荐

