如何将CameraX与SurfaceView/TextureView配合使用?Camera API迁移至CameraX时的预览组件替代方案及显式配置方法
Great question! It makes total sense to avoid relying on an alpha-stage component like PreviewView if you prefer more control over your preview surface. While PreviewView wraps SurfaceView/TextureView under the hood, you can absolutely use these views directly with CameraX by leveraging the SurfaceProvider interface. Let's break down how to do this for both SurfaceView and TextureView:
Using SurfaceView with CameraX
Step 1: Add SurfaceView to your layout
First, include a SurfaceView in your XML layout (or create it programmatically):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <SurfaceView android:id="@+id/camera_surface_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
Step 2: Set up CameraX and SurfaceHolder Callback
In your Activity/Fragment, get a reference to the SurfaceView, then set up a SurfaceHolder.Callback to listen for when the surface is ready. Then you can bind the CameraX Preview to this surface:
class CameraActivity : AppCompatActivity() { private lateinit var surfaceView: SurfaceView private var cameraProvider: ProcessCameraProvider? = null private var preview: Preview? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_camera) surfaceView = findViewById(R.id.camera_surface_view) // Request camera permissions first (skip if already handled) if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { startCameraSetup() } else { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 100) } } private fun startCameraSetup() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ cameraProvider = cameraProviderFuture.get() bindPreview() }, ContextCompat.getMainExecutor(this)) } private fun bindPreview() { cameraProvider?.let { provider -> // Unbind any existing use cases first provider.unbindAll() // Create Preview use case preview = Preview.Builder() // Optional: Set preferred resolution to match SurfaceView size .setResolutionSelector(ResolutionSelector.Builder() .setPreferredAspectRatio(AspectRatio.RATIO_16_9) .build()) .build() // Configure SurfaceProvider to use our SurfaceView's surface preview?.setSurfaceProvider { request -> val surfaceHolder = surfaceView.holder // Check if the surface is valid before providing it if (surfaceHolder.surface.isValid) { request.provideSurface(surfaceHolder.surface, ContextCompat.getMainExecutor(this)) { // Handle surface release if needed } } else { // Listen for surface creation if it's not ready yet surfaceHolder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { if (holder.surface.isValid) { request.provideSurface(holder.surface, ContextCompat.getMainExecutor(this)) { holder.removeCallback(this) } } } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {} override fun surfaceDestroyed(holder: SurfaceHolder) {} }) } } // Select back camera as default val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA // Bind the Preview use case to the camera try { provider.bindToLifecycle(this, cameraSelector, preview) } catch(exc: Exception) { Log.e("CameraX", "Use case binding failed", exc) } } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == 100 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startCameraSetup() } } }
Using TextureView with CameraX
The process is very similar to SurfaceView, but we'll use TextureView.SurfaceTextureListener instead:
Step 1: Add TextureView to your layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextureView android:id="@+id/camera_texture_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
Step 2: Set up CameraX and SurfaceTextureListener
class CameraActivity : AppCompatActivity() { private lateinit var textureView: TextureView private var cameraProvider: ProcessCameraProvider? = null private var preview: Preview? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_camera) textureView = findViewById(R.id.camera_texture_view) if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { startCameraSetup() } else { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 100) } } private fun startCameraSetup() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ cameraProvider = cameraProviderFuture.get() bindPreview() }, ContextCompat.getMainExecutor(this)) } private fun bindPreview() { cameraProvider?.let { provider -> provider.unbindAll() preview = Preview.Builder() .setResolutionSelector(ResolutionSelector.Builder() .setPreferredAspectRatio(AspectRatio.RATIO_16_9) .build()) .build() preview?.setSurfaceProvider { request -> val surfaceTexture = textureView.surfaceTexture if (surfaceTexture != null) { val surface = Surface(surfaceTexture) request.provideSurface(surface, ContextCompat.getMainExecutor(this)) { surface.release() } } else { textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { val cameraSurface = Surface(surface) request.provideSurface(cameraSurface, ContextCompat.getMainExecutor(this)) { cameraSurface.release() textureView.surfaceTextureListener = null } } override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {} override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = true override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {} } } } val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { provider.bindToLifecycle(this, cameraSelector, preview) } catch(exc: Exception) { Log.e("CameraX", "Use case binding failed", exc) } } } // Permission handling same as SurfaceView example override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == 100 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startCameraSetup() } } }
Key Notes & Best Practices
- Surface Lifecycle: Always make sure the surface is valid (created, not destroyed) before passing it to CameraX. The callbacks (
SurfaceHolder.Callback/SurfaceTextureListener) help you handle this correctly. - Resolution Matching: Use
ResolutionSelectorin the Preview.Builder to align the camera's output resolution with your view's aspect ratio—this prevents stretching or cropping of the preview. - Resource Cleanup: When the surface is released, make sure to clean up any
Surfaceobjects you create (like in the TextureView example) to avoid memory leaks. - Lifecycle Binding: Binding the use case to your Activity/Fragment lifecycle ensures CameraX handles camera resource management automatically when your app goes into the background.
内容的提问来源于stack exchange,提问作者Deepu George Jacob

