如何在Kotlin中用ImageButton实现CameraX摄像头切换
解决CameraX前后摄像头切换问题(含预览空指针修复)
以下是针对你的CameraX应用添加前后摄像头切换功能的完整解决方案,同时修复预览空指针问题:
关键修改步骤
1. 添加状态跟踪变量
在类成员变量中添加以下两个变量,用于跟踪当前摄像头朝向和CameraProvider实例:
private var currentLensFacing = CameraSelector.LENS_FACING_BACK private var cameraProvider: ProcessCameraProvider? = null
2. 绑定切换按钮点击事件
在onCreate方法中添加切换按钮的点击监听,并初始化图标:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) // ... 原有代码 ... // 添加摄像头切换按钮监听 binding.btnSwitchCamera.setOnClickListener { switchCamera() } // 初始化摄像头切换图标 updateSwitchCameraIcon() }
3. 修改startCamera函数
重构startCamera函数,使其支持传入摄像头朝向,并在绑定新用例前解绑所有旧用例(这是修复预览空指针的核心):
private fun startCamera(lensFacing: Int) { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ cameraProvider = cameraProviderFuture.get() // 解绑所有已绑定的相机用例,避免冲突 cameraProvider?.unbindAll() imagePreview = Preview.Builder().apply { setTargetAspectRatio(AspectRatio.RATIO_16_9) setTargetRotation(binding.previewView.display.rotation) }.build() imageAnalysis = ImageAnalysis.Builder().apply { setImageQueueDepth(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) }.build() imageAnalysis?.setAnalyzer(cameraExecutor, LuminosityAnalyzer()) imageCapture = ImageCapture.Builder().apply { setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) setFlashMode(ImageCapture.FLASH_MODE_AUTO) }.build() videoCapture = VideoCapture.Builder().apply { setTargetAspectRatio(AspectRatio.RATIO_16_9) }.build() val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build() val camera = cameraProvider?.bindToLifecycle( this, cameraSelector, imagePreview, imageCapture, videoCapture ) ?: return binding.previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE imagePreview?.setSurfaceProvider(binding.previewView.surfaceProvider) cameraControl = camera.cameraControl cameraInfo = camera.cameraInfo setTorchStateObserver() setZoomStateObserver() // 更新切换按钮图标 updateSwitchCameraIcon() }, ContextCompat.getMainExecutor(this)) }
4. 实现摄像头切换逻辑
添加切换摄像头的核心函数,切换朝向并重新启动相机:
private fun switchCamera() { // 切换当前摄像头朝向 currentLensFacing = if (currentLensFacing == CameraSelector.LENS_FACING_BACK) { CameraSelector.LENS_FACING_FRONT } else { CameraSelector.LENS_FACING_BACK } // 重新初始化相机 startCamera(currentLensFacing) }
5. 实现切换按钮图标更新
添加函数根据当前摄像头朝向更新按钮图标:
private fun updateSwitchCameraIcon() { val iconRes = if (currentLensFacing == CameraSelector.LENS_FACING_BACK) { // 替换为你的前置摄像头图标资源ID R.drawable.ic_camera_front_24dp } else { // 替换为你的后置摄像头图标资源ID R.drawable.ic_camera_back_24dp } binding.btnSwitchCamera.setImageDrawable(ContextCompat.getDrawable(this, iconRes)) }
6. 更新初始相机启动调用
修改onCreate和onRequestPermissionsResult中的startCamera()调用,传入当前摄像头朝向:
// onCreate中 if (allPermissionsGranted()) { startCamera(currentLensFacing) } // onRequestPermissionsResult中 if (allPermissionsGranted()) { startCamera(currentLensFacing) }
完整修改后代码
private val cameraExecutor = Executors.newSingleThreadExecutor() private var imagePreview: Preview? = null private var imageAnalysis: ImageAnalysis? = null private var imageCapture: ImageCapture? = null private var videoCapture: VideoCapture? = null private lateinit var outputDirectory: File private var cameraControl: CameraControl? = null private var cameraInfo: CameraInfo? = null private var linearZoom = 0f private var recording = false // 添加状态跟踪变量 private var currentLensFacing = CameraSelector.LENS_FACING_BACK private var cameraProvider: ProcessCameraProvider? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) if (allPermissionsGranted()) { startCamera(currentLensFacing) } else { requestPermissions( REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS ) } outputDirectory = getOutputDirectory() binding.cameraCaptureButton.setOnClickListener { takePicture() } initCameraModeSelector() binding.cameraTorchButton.setOnClickListener { toggleTorch() } // 添加摄像头切换按钮监听 binding.btnSwitchCamera.setOnClickListener { switchCamera() } // 初始化摄像头切换图标 updateSwitchCameraIcon() } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_CODE_PERMISSIONS) { if (allPermissionsGranted()) { startCamera(currentLensFacing) } else { finish() } } } private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED } private fun startCamera(lensFacing: Int) { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ cameraProvider = cameraProviderFuture.get() // 解绑所有已绑定的相机用例,避免冲突 cameraProvider?.unbindAll() imagePreview = Preview.Builder().apply { setTargetAspectRatio(AspectRatio.RATIO_16_9) setTargetRotation(binding.previewView.display.rotation) }.build() imageAnalysis = ImageAnalysis.Builder().apply { setImageQueueDepth(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) }.build() imageAnalysis?.setAnalyzer(cameraExecutor, LuminosityAnalyzer()) imageCapture = ImageCapture.Builder().apply { setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) setFlashMode(ImageCapture.FLASH_MODE_AUTO) }.build() videoCapture = VideoCapture.Builder().apply { setTargetAspectRatio(AspectRatio.RATIO_16_9) }.build() val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build() val camera = cameraProvider?.bindToLifecycle( this, cameraSelector, imagePreview, imageCapture, videoCapture ) ?: return binding.previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE imagePreview?.setSurfaceProvider(binding.previewView.surfaceProvider) cameraControl = camera.cameraControl cameraInfo = camera.cameraInfo setTorchStateObserver() setZoomStateObserver() // 更新切换按钮图标 updateSwitchCameraIcon() }, ContextCompat.getMainExecutor(this)) } private fun setTorchStateObserver() { cameraInfo?.torchState?.observe(this, { state -> if (state == TorchState.ON) { binding.cameraTorchButton.setImageDrawable( ContextCompat.getDrawable( this, R.drawable.ic_flashlight_off_24dp ) ) } else { binding.cameraTorchButton.setImageDrawable( ContextCompat.getDrawable( this, R.drawable.ic_flashlight_on_24dp ) ) } }) } private fun setZoomStateObserver() { cameraInfo?.zoomState?.observe(this, { state -> Log.d(TAG, "${state.linearZoom}") }) } private fun initCameraModeSelector() { binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab?) {} override fun onTabUnselected(tab: TabLayout.Tab?) {} override fun onTabSelected(tab: TabLayout.Tab?) { when (tab?.position) { PHOTO -> { binding.cameraCaptureButton.setOnClickListener { takePicture() } } VIDEO -> { binding.cameraCaptureButton.setOnClickListener { if (recording) { videoCapture?.stopRecording() it.isSelected = false recording = false } else { recordVideo() it.isSelected = true recording = true } } } } } }) } private fun takePicture() { val file = createFile( outputDirectory, FILENAME, PHOTO_EXTENSION ) val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build() imageCapture?.takePicture(outputFileOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { val msg = "Photo capture succeeded: ${file.absolutePath}" binding.previewView.post { Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show() } } override fun onError(exception: ImageCaptureException) { val msg = "Photo capture failed: ${exception.message}" binding.previewView.post { Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show() } } }) } private fun recordVideo() { val file = createFile( outputDirectory, FILENAME, VIDEO_EXTENSION ) val outputFileOptions = VideoCapture.OutputFileOptions.Builder(file).build() videoCapture?.startRecording(outputFileOptions, cameraExecutor, object : VideoCapture.OnVideoSavedCallback { override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) { val msg = "Video capture succeeded: ${file.absolutePath}" binding.previewView.post { Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show() } } override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) { val msg = "Video capture failed: $message" binding.previewView.post { Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show() } } }) } private fun toggleTorch() { if (cameraInfo?.torchState?.value == TorchState.ON) { cameraControl?.enableTorch(false) } else { cameraControl?.enableTorch(true) } } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { return when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP -> { if (linearZoom <= 0.9) { linearZoom += 0.1f } cameraControl?.setLinearZoom(linearZoom) true } KeyEvent.KEYCODE_VOLUME_DOWN -> { if (linearZoom >= 0.1) { linearZoom -= 0.1f } cameraControl?.setLinearZoom(linearZoom) true } else -> super.onKeyDown(keyCode, event) } } private class LuminosityAnalyzer : ImageAnalysis.Analyzer { private var lastAnalyzedTimestamp = 0L private fun ByteBuffer.toByteArray(): ByteArray { rewind() val data = ByteArray(remaining()) get(data) return data } override fun analyze(image: ImageProxy) { image.imageInfo.rotationDegrees val currentTimestamp = System.currentTimeMillis() if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) { val buffer = image.planes[0].buffer val data = buffer.toByteArray() val pixels = data.map { it.toInt() and 0xFF } val luma = pixels.average() Log.d("CameraXApp", "Average luminosity: $luma") lastAnalyzedTimestamp = currentTimestamp } image.close() } } override fun onDestroy() { super.onDestroy() cameraExecutor.shutdown() } private fun getOutputDirectory(): File { val mediaDir = externalMediaDirs.firstOrNull()?.let { File(it, resources.getString(R.string.app_name)).apply { mkdirs() } } return if (mediaDir != null && mediaDir.exists()) mediaDir else filesDir } // 新增摄像头切换相关函数 private fun switchCamera() { currentLensFacing = if (currentLensFacing == CameraSelector.LENS_FACING_BACK) { CameraSelector.LENS_FACING_FRONT } else { CameraSelector.LENS_FACING_BACK } startCamera(currentLensFacing) } private fun updateSwitchCameraIcon() { val iconRes = if (currentLensFacing == CameraSelector.LENS_FACING_BACK) { R.drawable.ic_camera_front_24dp // 替换为你的前置图标 } else { R.drawable.ic_camera_back_24dp // 替换为你的后置图标 } binding.btnSwitchCamera.setImageDrawable(ContextCompat.getDrawable(this, iconRes)) } companion object { private const val TAG = "MainActivity" private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS" private const val PHOTO_EXTENSION = ".jpg" private const val VIDEO_EXTENSION = ".mp4" private const val REQUEST_CODE_PERMISSIONS = 10 private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) private const val PHOTO = 0 private const val VIDEO = 1 fun createFile(baseFolder: File, format: String, extension: String) = File( baseFolder, SimpleDateFormat(format, Locale.US) .format(System.currentTimeMillis()) + extension ) }
注意事项
- 请确保你的布局文件中存在
btn_switch_camera这个ImageButton - 替换代码中的图标资源ID为你项目中实际的图标资源
- 切换时先解绑所有用例是修复预览空指针的关键,避免了CameraProvider重复绑定导致的资源冲突
内容的提问来源于stack exchange,提问作者user19697344
相关产品推荐
相关产品推荐

