Android 使用 CameraX 实现自定义相机拍照(一)

5,869 阅读8分钟

前言

在项目中用到了好几次使用 CameraX 来实现自定义相机的功能,每次写都是打开官方文档抄一遍,从来没有自己整理过,这次就把代码整理一下,下次抄代码就可以直接抄自己的了,不用去官方复制了。

代码实现

创建项目

新建一个名称为 CameraXApp 的工程,因为 CameraX 兼容的最低版本是 Android 5.0,所以 Minimum SDK 这里我选择的是 API 21,compileSdk 和 targetSdk 跟随默认设置使用的是 API 34。把默认生成的 Compose 代码删掉,重新建一个 TakePhotoActivity,将主题修改为“Theme.AppCompat.Light.NoActionBar”,并将其设置为启动 Activity。

class TakePhotoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_take_photo)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
}
}

添加依赖

把下边的依赖添加到 gradle/libs.versions.toml 文件里,然后在项目的 app/build.gradle.jks 里引入依赖。

// In your libs.versions.toml file
[versions]
camerax = "1.3.4"

[libraries]
camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" }
camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
camera-video = { group = "androidx.camera", name = "camera-video", version.ref = "camerax" }
camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "camerax" }
// In your app/build.gradle.kts file
dependencies {
    implementation(libs.camera.core)
    implementation(libs.camera.camera2)
    implementation(libs.camera.lifecycle)
    implementation(libs.camera.video)
    implementation(libs.camera.view)
    implementation(libs.camera.extensions)
}

现在最新版本的 Android Studio 已经默认使用 Version Catalog 来管理依赖了,如果还不了解的话,可以看看这篇文章《Android 使用 Version Catalog 管理依赖项》

设置布局

打开 res/layout/activity_take_photo.xml 布局文件,并将其替换为以下代码。这里添加了一个用来预览相机的 PreviewView,底部居中添加了一个拍照按钮,右上角添加了一个用于翻转相机的按钮。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black"
    tools:context=".TakePhotoActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ImageButton
        android:id="@+id/cameraCaptureBtn"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginBottom="20dp"
        android:background="@drawable/take_photo_btn"
        android:scaleType="fitCenter"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        tools:ignore="ContentDescription" />

    <ImageButton
        android:id="@+id/flipCameraBtn"
        android:layout_width="28dp"
        android:layout_height="28dp"
        android:layout_margin="16dp"
        android:background="@drawable/flip_camera_btn"
        android:scaleType="fitCenter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

权限请求

因为拍照需要相机权限,所以我们需要在 AndroidManifest.xml 里添加以下对应权限。

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

添加 android.hardware.camera.any 的作用是确保设备配有相机。这里讲下 android.hardware.camera.any 和 android.hardware.camera 这两个 feature 的区别:加 any 的意思只要设备有相机就行,不要求相机是前置还是后置;不加 any 的话,代表要求设备必须配有相机且必须配有后置相机。

再修改一下 TakePhotoActivity 类,添加动态申请相机权限的代码。运行一下项目,就能看到页面弹出了权限请求弹窗,用户选择授予权限以后就能执行后续操作了。

private var PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)

class TakePhotoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        viewBinding = ActivityTakePhotoBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
checkPermissions()
    }

    private fun checkPermissions() {
        if (!hasPermissions(this)) {
            activityResultLauncher.launch(PERMISSIONS_REQUIRED)
        }
    }

    private val activityResultLauncher =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions())
        { permissions ->
var permissionGranted = true
            permissions.entries.forEach {
if (it.key in PERMISSIONS_REQUIRED && !it.value)
                    permissionGranted = false
            }
if (!permissionGranted) {
                Toast.makeText(this, "Permission request denied", Toast.LENGTH_LONG).show()
            } else {
                Toast.makeText(this, "Permission request granted", Toast.LENGTH_LONG).show()
            }
        }

companion object {

        fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
}
}

相机预览

实现相机预览时会用到 viewBinding 来简化 View 的初始化过程,所以需要先检查一下 viewBinding 特性是否启用了,没启用记得加一下下面的代码。

buildFeatures {
viewBinding = true
} 

接着修改 TakePhotoActivity 类,用于实现相机预览功能。简单解释一下下面的代码:在用户授予权限以后,执行了相机的初始化操作,将相机的生命周期和页面的生命周期绑定到了一起,这样 CameraX 自己就能感知到生命周期,就不需要我们再写打开和关闭相机这样的代码了;同时我们新加了是否有后置摄像头、是否有前置摄像头这样的判断方法。这时运行一下项目的话,就能看到相机的预览效果了。

private var PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)

class TakePhotoActivity : AppCompatActivity() {

    private lateinit var viewBinding: ActivityTakePhotoBinding

    private var cameraProvider: ProcessCameraProvider? = null

    private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        viewBinding = ActivityTakePhotoBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
checkPermissions()
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener( {
cameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder()
                .build()
                .also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
                }
cameraSelector = if (hasBackCamera()) {
                CameraSelector.DEFAULT_BACK_CAMERA
            } else {
                CameraSelector.DEFAULT_FRONT_CAMERA
            }
            try {
                cameraProvider?.unbindAll()
                cameraProvider?.bindToLifecycle(
                    this, cameraSelector, preview
                )
            } catch (exc: Exception) {
                Log.e(TAG, "Use case binding failed.", exc)
            }
        } , ContextCompat.getMainExecutor(this))
    }

    private fun hasBackCamera(): Boolean {
        return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
    }

    private fun hasFrontCamera(): Boolean {
        return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
    }

    private fun checkPermissions() {
        if (!hasPermissions(this)) {
            activityResultLauncher.launch(PERMISSIONS_REQUIRED)
        } else {
            startCamera()
        }
    }

    private val activityResultLauncher =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions())
        { permissions ->
var permissionGranted = true
            permissions.entries.forEach {
if (it.key in PERMISSIONS_REQUIRED && !it.value)
                    permissionGranted = false
            }
if (!permissionGranted) {
                //这里应该实现一个Dialog,提示用户去设置中打开权限
                Toast.makeText(this, "Permission request denied", Toast.LENGTH_LONG).show()
            } else {
                startCamera()
            }
        }

companion object {
        const val TAG = "TakePhotoActivity"
        fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
}
}

拍照功能

接下来写实现拍照功能的代码。可以看到,下面的代码新添加了 takePhoto 方法,takePhoto 方法里先判断了imageCapture 对象,表明拍照操作必须在 imageCapture 对象初始化之后才能执行,imageCapture 对象是在startCamera 方法里初始化并作为 bindToLifecycle 方法的参数使用的,outputOptions 指定了所拍摄的图片存放的位置,cameraExecutor 指定了回调方法在子线程。运行一下项目,就可以使用拍照功能了。

private var PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)

class TakePhotoActivity : AppCompatActivity() {

    private lateinit var viewBinding: ActivityTakePhotoBinding

    private var cameraProvider: ProcessCameraProvider? = null
    private var imageCapture: ImageCapture? = null

    private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
    private var cameraExecutor = Executors.newSingleThreadExecutor()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        viewBinding = ActivityTakePhotoBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
checkPermissions()
        viewBinding.cameraCaptureBtn.setOnClickListener { takePhoto() }
}

    private fun takePhoto() {
        val imageCapture = imageCapture ?: return

        val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
        val photoFile = File(
            getExternalFilesDir(null), "$name.jpg"
        )
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        imageCapture.takePicture(outputOptions,
            cameraExecutor,
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val msg = "Photo capture succeeded: ${output.savedUri}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)
                }
            })
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener( {
cameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder().build().also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
            }
imageCapture = ImageCapture.Builder().build()

            cameraSelector = if (hasBackCamera()) {
                CameraSelector.DEFAULT_BACK_CAMERA
            } else {
                CameraSelector.DEFAULT_FRONT_CAMERA
            }
            try {
                cameraProvider?.unbindAll()
                cameraProvider?.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture
                )
            } catch (exc: Exception) {
                Log.e(TAG, "Use case binding failed.", exc)
            }
        } , ContextCompat.getMainExecutor(this))
    }

    private fun hasBackCamera(): Boolean {
        return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
    }

    private fun hasFrontCamera(): Boolean {
        return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
    }

    private fun checkPermissions() {
        if (!hasPermissions(this)) {
            activityResultLauncher.launch(PERMISSIONS_REQUIRED)
        } else {
            startCamera()
        }
    }

    private val activityResultLauncher =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
var permissionGranted = true
            permissions.entries.forEach {
if (it.key in PERMISSIONS_REQUIRED && !it.value) permissionGranted = false
            }
if (!permissionGranted) {
                //这里应该实现一个Dialog,提示用户去设置中打开权限
                Toast.makeText(this, "Permission request denied", Toast.LENGTH_LONG).show()
            } else {
                startCamera()
            }
        }

override fun onDestroy() {
        super.onDestroy()
        cameraProvider?.unbindAll()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val TAG = "TakePhotoActivity"
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
}
}

镜头翻转

flipCamera 方法就是用来实现镜头翻转效果的。在翻转镜头时,首先校验了设备是否前置和后置摄像头都有,都有才能执行翻转操作;然后解绑之前的摄像头,重新绑定新的摄像头。

private var PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)

class TakePhotoActivity : AppCompatActivity() {

    private lateinit var viewBinding: ActivityTakePhotoBinding

    private var cameraProvider: ProcessCameraProvider? = null
    private var imageCapture: ImageCapture? = null

    private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

private var cameraExecutor = Executors.newSingleThreadExecutor()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        viewBinding = ActivityTakePhotoBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
checkPermissions()
        viewBinding.cameraCaptureBtn.setOnClickListener { takePhoto() }
viewBinding.flipCameraBtn.setOnClickListener { flipCamera() }
}

    private fun takePhoto() {
        val imageCapture = imageCapture ?: return

        val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
        val photoFile = File(
            getExternalFilesDir(null), "$name.jpg"
        )
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        imageCapture.takePicture(outputOptions,
            cameraExecutor,
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val msg = "Photo capture succeeded: ${output.savedUri}"
                    Log.d(TAG, msg)
                }
            })
    }

    private fun flipCamera() {
        if (hasFrontCamera() && hasBackCamera()) {
            startCamera(true)
        }
    }

    private fun startCamera(isFlipCamera: Boolean) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener( {
cameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder().build().also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
            }
imageCapture = ImageCapture.Builder().build()
            cameraSelector = getCameraSelector(isFlipCamera)
            try {
                cameraProvider?.unbindAll()
                cameraProvider?.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture
                )
            } catch (exc: Exception) {
                Log.e(TAG, "Use case binding failed.", exc)
            }
        } , ContextCompat.getMainExecutor(this))
    }

    private fun getCameraSelector(isFlipCamera: Boolean): CameraSelector {
        return if (isFlipCamera) {
            if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
                CameraSelector.DEFAULT_FRONT_CAMERA
} else {
                CameraSelector.DEFAULT_BACK_CAMERA
}
        } else {
            if (hasBackCamera()) {
                CameraSelector.DEFAULT_BACK_CAMERA
} else {
                CameraSelector.DEFAULT_FRONT_CAMERA
}
        }
    }

    private fun hasBackCamera(): Boolean {
        return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
    }

    private fun hasFrontCamera(): Boolean {
        return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
    }

    private fun checkPermissions() {
        if (!hasPermissions(this)) {
            activityResultLauncher.launch(PERMISSIONS_REQUIRED)
        } else {
            startCamera(false)
        }
    }

    private val activityResultLauncher =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
var permissionGranted = true
            permissions.entries.forEach {
if (it.key in PERMISSIONS_REQUIRED && !it.value) permissionGranted = false
            }
if (!permissionGranted) {
                //这里应该实现一个Dialog,提示用户去设置中打开权限
                Toast.makeText(this, "Permission request denied", Toast.LENGTH_LONG).show()
            } else {
                startCamera(false)
            }
        }

override fun onDestroy() {
        super.onDestroy()
        cameraProvider?.unbindAll()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val TAG = "TakePhotoActivity"
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
 }
}
}

闪光灯

bindToLifecycle 方法会返回当前摄像头的 Camera 对象,通过 camera.cameraInfo.hasFlashUnit() 来判断有没有闪光灯,设备的后置摄像头一般都有闪光灯,前置摄像头一般都没有闪光灯。

private var camera: Camera? = null

camera = cameraProvider?.bindToLifecycle(
    this, cameraSelector, preview, imageCapture
)

private fun switchFlash() {
    camera?.let {
if (it.cameraInfo.hasFlashUnit()) {
            if (it.cameraInfo.torchState.getValue() == 0) {
                it.cameraControl.enableTorch(true)
            } else {
                it.cameraControl.enableTorch(false)
            }
        }
    }
}

更多要求

图片旋转

在测试的时候,发现有些设备明明是竖着拍的照片,拍完以后却发现图片是横着的,这时候需要我们手动对图片做一下旋转。takePicture 有两个方法,一个方法传参是 OnImageCapturedCallback,ImageCapture.OnImageCapturedCallback 回调返回的是 ImageProxy,另一个方法传参是 OnImageSavedCallback,ImageCapture.OnImageSavedCallback 回调返回的是 ImageCapture.OutputFileResults。

如果你传参是 ImageCapture.OnImageCapturedCallback,要对图片做旋转,只要获取到 image 的 Bitmap 以后,通过 Matrix 对Bitmap 做旋转,再将 Bitmap 存到本地文件中就行了。代码逻辑如下所示。

private fun takePhoto() {
    val imageCapture = imageCapture ?: return

    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
    val photoFile = File(getExternalFilesDir(null), "$name.jpg")
    imageCapture.takePicture(cameraExecutor, object : ImageCapture.OnImageCapturedCallback() {
        override fun onCaptureSuccess(image: ImageProxy) {
            super.onCaptureSuccess(image)
            val matrix = Matrix().apply {
postRotate(image.imageInfo.rotationDegrees.toFloat())
            }
val bitmapRotated = Bitmap.createBitmap(
                image.toBitmap(), 0, 0, image.width, image.height, matrix, true
            )
            saveBitmapToFile(bitmapRotated, photoFile)
            image.close()
        }
    })
}

fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean {
    var out: FileOutputStream? = null
    return try {
        out = FileOutputStream(file)
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
        true
    } catch (e: Exception) {
        e.printStackTrace()
        false
    } finally {
        try {
            out?.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
}

如果你使用的是 ImageCapture.OnImageSavedCallback,要对图片做旋转,其实逻辑和上面是一样的,只是要先将本地图片文件解码成 Bitmap,再通过 ExifInterface 读取到本地图片的 orientation,再使用 Matrix 旋转图片。代码逻辑如下所示。

private fun takePhoto() {
    val imageCapture = imageCapture ?: return

    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
    val photoFile = File(getExternalFilesDir(null), "$name.jpg")

    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
    imageCapture.takePicture(outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
            val savedUri = outputFileResults.savedUri ?: return
            rotateImage(savedUri.path ?: "", photoFile)
        }

        override fun onError(exception: ImageCaptureException) {
            Log.e(TAG, "Photo capture failed: ${exception.message}", exception)
        }
    })
}

private fun rotateImage(photoPath: String, file: File): Boolean {
    var bitmap: Bitmap? = null
    try {
        val exif = ExifInterface(photoPath)
        val orientation = exif.getAttributeInt(
            ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL
        )

        val matrix = Matrix()
        when (orientation) {
            ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
            ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
            ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
        }

        bitmap = BitmapFactory.decodeFile(photoPath)
        val rotatedBitmap = Bitmap.createBitmap(
            bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
        )
        bitmap.recycle()
        return saveBitmapToFile(rotatedBitmap, file)
    } catch (e: Exception) {
        e.printStackTrace()
        return false
    } finally {
        bitmap?.recycle()
    }
}

如果要操作图片旋转的话,参数还是传 ImageCapture.OnImageCapturedCallback 吧,省去了将本地图片解码成 bitmap 这步骤。不过通过 ExifInterface 读取本地图片的 EXIF 信息(比如图像的方向、拍摄时间、设备信息等),这个知识点大家还是要知道的,这个很常用。

图片镜像

如果是前置摄像头拍摄的照片,拍摄以后本地保存的图片默认是镜像的。镜像是啥意思呢?就是你预览的时候,某个物体在图片的左边,等你去看拍完的图片时,发现这个物体在图片的右边。如果你不想让前置摄像头拍出来的照片是镜像的,可以使用 postScale 做一下水平翻转。不过一般不用特殊处理图片的镜像效果,有些场景更是希望保留图片的镜像效果,比如使用前置摄像头拍照文字时,预览时手机上看到的文字是反的,但保存到本地的图片上的字是正的。

imageCapture.takePicture(cameraExecutor, object : ImageCapture.OnImageCapturedCallback() {
    override fun onCaptureSuccess(image: ImageProxy) {
        super.onCaptureSuccess(image)
        val matrix = Matrix().apply {
postRotate(image.imageInfo.rotationDegrees.toFloat())
            if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) {
                postScale(-1f, 1f)
            }
        }
val bitmapRotated = Bitmap.createBitmap(
            image.toBitmap(), 0, 0, image.width, image.height, matrix, true
        )
        saveBitmapToFile(bitmapRotated, photoFile)
        image.close()
    }
})

图片压缩

有时候,我们拍完的照片在上传到服务端前需要本地做一下压缩,比如将图片的分辨率压缩到 1920*1080 以下,总质量在 500KB 以下。压缩方案也就是平时我们用的比较多的质量压缩、采样率压缩这样的方案,同时也可以结合 Matrix 或 decodeFile 时使用 RGB_565等手段。这些都是比较常见的代码,这里就不具体实现了。

结论

本文实现了相机权限的动态请求、相机的视图预览、相机的自定义拍照、前后摄像头翻转、闪光灯这些功能,还提到了一些可能用到的功能,比如保持照片方向一直保持竖直、对前置摄像头拍摄的照片进行翻转、对图片进行压缩。

除了以上功能,我们可能还需要:双指缩放预览视图、单击聚焦预览视图、设置分辨率为 4:3 或 16:9、预览视图的全屏显示、图片保存到系统相册等这些功能。这一篇写不完了,下一篇再写吧,不然文章就太长了。

希望这篇文章对你有所帮助,下次见!

参考资料

Google. CameraX 概览. developer.android.com/media/camer…

Google. CameraX 使用入门. developer.android.com/codelabs/ca…

Google. Camera-Samples. github.com/android/cam…

github.com/Coding-Meet…