You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 12:54:17