前言
在项目中用到了好几次使用 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…