Android CameraX PreviewView截图黑屏,求完整Activity截图方案
解决CameraX PreviewView截图黑屏问题
问题根源
CameraX的PreviewView通过SurfaceTexture实现硬件加速的相机预览渲染,而View.draw(Canvas)仅能捕获View树中软件绘制的内容,无法读取硬件层渲染的相机画面,因此截图时预览区域会显示黑屏。
解决方案一:结合ImageCapture与界面截图合成
通过CameraX的ImageCapture捕获当前相机帧,再截取Activity界面,最后将两者合成为完整截图,适合静态场景。
1. 配置CameraX添加ImageCapture用例
private lateinit var imageCapture: ImageCapture private fun startCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider = cameraProviderFuture.get() val preview = Preview.Builder() .build() .also { it.setSurfaceProvider(binding.previewView.surfaceProvider) } // 初始化ImageCapture imageCapture = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build() val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) } catch(exc: Exception) { Log.e("CameraX", "绑定用例失败", exc) } }, ContextCompat.getMainExecutor(this)) }
2. 实现截图合成逻辑
private fun captureFullScreenWithPreview() { // 1. 捕获相机帧 val photoFile = File(externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg") val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() imageCapture.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { // 2. 截取Activity界面(不含预览内容) val activityBitmap = captureActivityScreenshot() // 3. 读取相机帧Bitmap val cameraBitmap = BitmapFactory.decodeFile(photoFile.absolutePath) // 4. 合成完整截图 val combinedBitmap = combineBitmaps(activityBitmap, cameraBitmap, binding.previewView) // 保存合成后的图片 saveBitmap(combinedBitmap) } override fun onError(exception: ImageCaptureException) { Log.e("Capture", "捕获相机帧失败", exception) } } ) } // 截取Activity界面 private fun captureActivityScreenshot(): Bitmap { val rootView = window.decorView.rootView rootView.isDrawingCacheEnabled = true val bitmap = Bitmap.createBitmap(rootView.drawingCache) rootView.isDrawingCacheEnabled = false return bitmap } // 合成相机帧与界面截图 private fun combineBitmaps(activityBitmap: Bitmap, cameraBitmap: Bitmap, previewView: PreviewView): Bitmap { val combinedBitmap = Bitmap.createBitmap(activityBitmap.width, activityBitmap.height, activityBitmap.config) val canvas = Canvas(combinedBitmap) // 先绘制界面截图 canvas.drawBitmap(activityBitmap, 0f, 0f, null) // 计算PreviewView在屏幕上的位置与尺寸 val location = IntArray(2) previewView.getLocationOnScreen(location) val left = location[0] val top = location[1] val width = previewView.width val height = previewView.height // 将相机帧缩放到预览区域尺寸后绘制 val scaledCameraBitmap = Bitmap.createScaledBitmap(cameraBitmap, width, height, true) canvas.drawBitmap(scaledCameraBitmap, left.toFloat(), top.toFloat(), null) scaledCameraBitmap.recycle() return combinedBitmap } // 保存Bitmap到本地 private fun saveBitmap(bitmap: Bitmap) { val saveFile = File(externalMediaDirs.first(), "full_screenshot_${System.currentTimeMillis()}.jpg") val outputStream = FileOutputStream(saveFile) bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) outputStream.flush() outputStream.close() bitmap.recycle() }
解决方案二:使用MediaProjection API
通过系统MediaProjection捕获整个屏幕内容(包含硬件层渲染的相机预览),适合需要实时或精准全屏截图的场景,但需要用户授权屏幕捕获权限。
1. 请求屏幕捕获权限
private val REQUEST_CODE_SCREEN_CAPTURE = 1001 private fun startScreenCapture() { val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager val intent = mediaProjectionManager.createScreenCaptureIntent() startActivityForResult(intent, REQUEST_CODE_SCREEN_CAPTURE) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_CODE_SCREEN_CAPTURE && resultCode == RESULT_OK) { val mediaProjection = (getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager) .getMediaProjection(resultCode, data!!) captureScreenWithMediaProjection(mediaProjection) } }
2. 捕获完整屏幕内容
private fun captureScreenWithMediaProjection(mediaProjection: MediaProjection) { val displayMetrics = resources.displayMetrics val width = displayMetrics.widthPixels val height = displayMetrics.heightPixels val imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1) val virtualDisplay = mediaProjection.createVirtualDisplay( "ScreenCapture", width, height, displayMetrics.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.surface, null, null ) imageReader.setOnImageAvailableListener({ reader -> val image = reader.acquireLatestImage() val buffer = image.planes[0].buffer val bytes = ByteArray(buffer.remaining()) buffer.get(bytes) val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) // 保存或使用截图 saveBitmap(bitmap) // 释放资源 image.close() reader.close() virtualDisplay.release() mediaProjection.stop() }, ContextCompat.getMainExecutor(this)) }
注意事项
- 方案一需处理相机帧与界面截图的同步问题,可能存在微小延迟,适合静态场景。
- 方案二需要用户授权屏幕捕获权限,Android 13+需额外申请
READ_MEDIA_IMAGES权限用于保存图片。 - 所有Bitmap操作完成后需调用
recycle()回收内存,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Lokesh Arora
相关产品推荐
相关产品推荐

