一、简介
本片文章主要针对官方提供的CameraX实现进行简要的解析,从而学会如何编写一个简单的CameraX实例的相机Demo。本篇文章会分析如下几大块:
- CameraX实例实现的关键变量
- 官方是如何对CameraX实例进行初始化的
- 官方是如何使用CameraX实例进行拍照的
- 释放实例等其他模块
相关文章和资源推荐:
- Android Camera系列文章目录索引汇总
- Android CameraX 综述
- 官方Demo传送门
- CameraX库发版记录
二、源码分析
2.1 build.gradle
- Kotlin_version:
androidx.core:core-ktx:1.6.0 - camerax_version:
1.1.0-alpha07
注意:
- 官方给的Demo已经很久没有更新过了,使用的camerax版本相对较老,请访问CameraX库发板记录获取最新代码。
- CameraX相关库大概每一个月更新一个版本。需定期及时关注并更新相应库。
- 目前本人使用CameraX最新的库即:
kotlin: 1.6.10 camerax : 1.2.0-alpha03
相关库的配置请参考官方或者我提供的对应版本。
2.2 代码结构
 代码结构很简单,核心类在CameraFragment ,其他可自己根据兴趣查看。
2.3 变量
 CameraFragment里重要参数如下:
变量 | 说明 |
---|
lensFacing | 相机ID | preview | 预览画面 | Image capture | 用于拍照处理 | Image Analyzer | 用于照片分析处理 | camera | 相机实例 | cameraProvider | 用于提供消息实例 |
2.3.1 lensFacing
该变量很好理解,为0或者1,分别对应如下
public static final int LENS_FACING_FRONT = 0;
public static final int LENS_FACING_BACK = 1;
于此同时也提供了相应的方法,来判断手机设备是否支持前置或者后置摄像头:
private fun hasBackCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}
private fun hasFrontCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}
2.3.2 preview
preview 的类型为Preview 。继承UseCase ,这里只要了解该变量提供了Camera 预览画面以及相关的操作方法,具体的阐述如下,具体的源码细节在后续相关文章中再逐步分析。 A use case that provides a camera preview stream for displaying on-screen.
2.3.3 Image capture
同preview 一样,Image capture 也继承于UseCase ,主要用于处理拍照相关的操作设置。
- A use case for taking a picture.
- This class is designed for basic picture taking.
- It provides takePicture() functions to take a picture to memory or save to a file, and provides image metadata.
2.3.4 Image Analyzer
继承于UseCase ,用于做图片分析功能的类,平时基本用不到
A use case providing CPU accessible images for an app to perform image analysis on.
2.3.5 camera
参看源码使用就好,用到的地方不多
The camera interface is used to control the flow of data to use cases, control the camera via the CameraControl, and publish the state of the camera via CameraInfo.
2.3.6 cameraProvider
A singleton which can be used to bind the lifecycle of cameras to any LifecycleOwner within an application’s process.
2.4 初始化流程
有一部分代码,作为变量的初始化,或者更改一些UI界面,线程切换,监听手机方向,按键广播等,这块每个APP有不同的实现方案,就不具体阐述了,感兴趣的可自行分析。核心的相机初始化流程氛围如下几部分:
- 获取cameraProvider实例【单例】
- 判断并设置lensFacing
- 实力化preview和imageCapture, ImageAnalyzer【可选】
- 生成camera实例
1,2步骤在方法setUpCamera ;3,4部分在bindCameraUseCases
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(Runnable {
cameraProvider = cameraProviderFuture.get()
lensFacing = when {
hasBackCamera() -> CameraSelector.LENS_FACING_BACK
hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
else -> throw IllegalStateException("Back and front camera are unavailable")
}
bindCameraUseCases()
}, ContextCompat.getMainExecutor(requireContext()))
private fun bindCameraUseCases(){
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
preview = Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
imageAnalyzer = ImageAnalysis.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
cameraProvider.unbindAll()
try {
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
2.5 拍照
使用Image Capture
cameraUiContainerBinding?.cameraCaptureButton?.setOnClickListener {
imageCapture?.let { imageCapture ->
******1.获取拍照输出文件******
val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)
val metadata = Metadata().apply {
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
******2.配置ImageCapture输出选项******
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
.setMetadata(metadata)
.build()
******3.调用takePicture方法******
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) {
******4.回调******
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
Log.d(TAG, "Photo capture succeeded: $savedUri")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
requireActivity().sendBroadcast(
Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)
)
}
})
}
}
2.6 Release
释放Camera实例方法很简单:
cameraProviderFuture.get().unbindAll();
三、相关练习
- git clone官方源码分析其他相关的类
- 自己参考官方源码完成一个CameraX实例Demo的实现。
附录 CameraFragment源码:
package com.android.example.cameraxbasic.fragments
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.ImageFormat
import android.graphics.drawable.ColorDrawable
import android.hardware.display.DisplayManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.*
import android.webkit.MimeTypeMap
import androidx.camera.core.*
import androidx.camera.core.ImageCapture.FLASH_MODE_ON
import androidx.camera.core.ImageCapture.Metadata
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.navigation.Navigation
import androidx.window.WindowManager
import com.android.example.cameraxbasic.KEY_EVENT_ACTION
import com.android.example.cameraxbasic.KEY_EVENT_EXTRA
import com.android.example.cameraxbasic.MainActivity
import com.android.example.cameraxbasic.R
import com.android.example.cameraxbasic.databinding.CameraUiContainerBinding
import com.android.example.cameraxbasic.databinding.FragmentCameraBinding
import com.android.example.cameraxbasic.utils.ANIMATION_FAST_MILLIS
import com.android.example.cameraxbasic.utils.ANIMATION_SLOW_MILLIS
import com.android.example.cameraxbasic.utils.simulateClick
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.ArrayDeque
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.collections.ArrayList
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
typealias LumaListener = (luma: Double) -> Unit
class CameraFragment : Fragment() {
private var _fragmentCameraBinding: FragmentCameraBinding? = null
private val fragmentCameraBinding get() = _fragmentCameraBinding!!
private var cameraUiContainerBinding: CameraUiContainerBinding? = null
private lateinit var outputDirectory: File
private lateinit var broadcastManager: LocalBroadcastManager
private var displayId: Int = -1
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private var preview: Preview? = null
private var imageCapture: ImageCapture? = null
private var imageAnalyzer: ImageAnalysis? = null
private var camera: Camera? = null
private var cameraProvider: ProcessCameraProvider? = null
private lateinit var windowManager: WindowManager
private val displayManager by lazy {
requireContext().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
}
private lateinit var cameraExecutor: ExecutorService
private val volumeDownReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.getIntExtra(KEY_EVENT_EXTRA, KeyEvent.KEYCODE_UNKNOWN)) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
cameraUiContainerBinding?.cameraCaptureButton?.simulateClick()
}
}
}
}
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
override fun onDisplayChanged(displayId: Int) = view?.let { view ->
if (displayId == this@CameraFragment.displayId) {
Log.d(TAG, "Rotation changed: ${view.display.rotation}")
imageCapture?.targetRotation = view.display.rotation
imageAnalyzer?.targetRotation = view.display.rotation
}
} ?: Unit
}
override fun onResume() {
super.onResume()
if (!PermissionsFragment.hasPermissions(requireContext())) {
Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
CameraFragmentDirections.actionCameraToPermissions()
)
}
}
override fun onDestroyView() {
_fragmentCameraBinding = null
super.onDestroyView()
cameraExecutor.shutdown()
broadcastManager.unregisterReceiver(volumeDownReceiver)
displayManager.unregisterDisplayListener(displayListener)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_fragmentCameraBinding = FragmentCameraBinding.inflate(inflater, container, false)
return fragmentCameraBinding.root
}
private fun setGalleryThumbnail(uri: Uri) {
cameraUiContainerBinding?.photoViewButton?.let { photoViewButton ->
photoViewButton.post {
photoViewButton.setPadding(resources.getDimension(R.dimen.stroke_small).toInt())
Glide.with(photoViewButton)
.load(uri)
.apply(RequestOptions.circleCropTransform())
.into(photoViewButton)
}
}
}
@SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
cameraExecutor = Executors.newSingleThreadExecutor()
broadcastManager = LocalBroadcastManager.getInstance(view.context)
val filter = IntentFilter().apply { addAction(KEY_EVENT_ACTION) }
broadcastManager.registerReceiver(volumeDownReceiver, filter)
displayManager.registerDisplayListener(displayListener, null)
windowManager = WindowManager(view.context)
outputDirectory = MainActivity.getOutputDirectory(requireContext())
fragmentCameraBinding.viewFinder.post {
displayId = fragmentCameraBinding.viewFinder.display.displayId
updateCameraUi()
setUpCamera()
}
fragmentCameraBinding.viewFinder.setOnTouchListener { v, event ->
if (event.action != MotionEvent.ACTION_UP) {
return@setOnTouchListener true
}
val factory = fragmentCameraBinding.viewFinder.meteringPointFactory;
val focusPoint = factory.createPoint(event.x, event.y,0.3f)
Log.i("sun_q","point = "+focusPoint.toString())
val focusAction = FocusMeteringAction.Builder(focusPoint).build()
camera?.cameraControl?.startFocusAndMetering(focusAction)
return@setOnTouchListener true
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
bindCameraUseCases()
updateCameraSwitchButton()
}
private fun setUpCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(Runnable {
cameraProvider = cameraProviderFuture.get()
lensFacing = when {
hasBackCamera() -> CameraSelector.LENS_FACING_BACK
hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
else -> throw IllegalStateException("Back and front camera are unavailable")
}
updateCameraSwitchButton()
bindCameraUseCases()
}, ContextCompat.getMainExecutor(requireContext()))
}
private fun bindCameraUseCases() {
val metrics = windowManager.getCurrentWindowMetrics().bounds
Log.d(TAG, "Screen metrics: ${metrics.width()} x ${metrics.height()}")
val screenAspectRatio = aspectRatio(metrics.width(), metrics.height())
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
val rotation = fragmentCameraBinding.viewFinder.display.rotation
val cameraProvider = cameraProvider
?: throw IllegalStateException("Camera initialization failed.")
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
preview = Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
imageAnalyzer = ImageAnalysis.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
cameraProvider.unbindAll()
try {
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = max(width, height).toDouble() / min(width, height)
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
private fun updateCameraUi() {
cameraUiContainerBinding?.root?.let {
fragmentCameraBinding.root.removeView(it)
}
cameraUiContainerBinding = CameraUiContainerBinding.inflate(
LayoutInflater.from(requireContext()),
fragmentCameraBinding.root,
true
)
lifecycleScope.launch(Dispatchers.IO) {
outputDirectory.listFiles { file ->
EXTENSION_WHITELIST.contains(file.extension.toUpperCase(Locale.ROOT))
}?.maxOrNull()?.let {
setGalleryThumbnail(Uri.fromFile(it))
}
}
cameraUiContainerBinding?.cameraCaptureButton?.setOnClickListener {
imageCapture?.let { imageCapture ->
val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)
val metadata = Metadata().apply {
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
.setMetadata(metadata)
.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 savedUri = output.savedUri ?: Uri.fromFile(photoFile)
Log.d(TAG, "Photo capture succeeded: $savedUri")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setGalleryThumbnail(savedUri)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
requireActivity().sendBroadcast(
Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)
)
}
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(savedUri.toFile().extension)
MediaScannerConnection.scanFile(
context,
arrayOf(savedUri.toFile().absolutePath),
arrayOf(mimeType)
) { _, uri ->
Log.d(TAG, "Image capture scanned into media store: $uri")
}
}
})
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fragmentCameraBinding.root.postDelayed({
fragmentCameraBinding.root.foreground = ColorDrawable(Color.WHITE)
fragmentCameraBinding.root.postDelayed(
{ fragmentCameraBinding.root.foreground = null }, ANIMATION_FAST_MILLIS)
}, ANIMATION_SLOW_MILLIS)
}
}
}
cameraUiContainerBinding?.cameraSwitchButton?.let {
it.isEnabled = false
it.setOnClickListener {
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) {
CameraSelector.LENS_FACING_BACK
} else {
CameraSelector.LENS_FACING_FRONT
}
bindCameraUseCases()
}
}
cameraUiContainerBinding?.photoViewButton?.setOnClickListener {
if (true == outputDirectory.listFiles()?.isNotEmpty()) {
Navigation.findNavController(
requireActivity(), R.id.fragment_container
).navigate(CameraFragmentDirections
.actionCameraToGallery(outputDirectory.absolutePath))
}
}
}
private fun updateCameraSwitchButton() {
try {
cameraUiContainerBinding?.cameraSwitchButton?.isEnabled = hasBackCamera() && hasFrontCamera()
} catch (exception: CameraInfoUnavailableException) {
cameraUiContainerBinding?.cameraSwitchButton?.isEnabled = false
}
}
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 class LuminosityAnalyzer(listener: LumaListener? = null) : ImageAnalysis.Analyzer {
private val frameRateWindow = 8
private val frameTimestamps = ArrayDeque<Long>(5)
private val listeners = ArrayList<LumaListener>().apply { listener?.let { add(it) } }
private var lastAnalyzedTimestamp = 0L
var framesPerSecond: Double = -1.0
private set
fun onFrameAnalyzed(listener: LumaListener) = listeners.add(listener)
private fun ByteBuffer.toByteArray(): ByteArray {
rewind()
val data = ByteArray(remaining())
get(data)
return data
}
override fun analyze(image: ImageProxy) {
if (listeners.isEmpty()) {
image.close()
return
}
val currentTime = System.currentTimeMillis()
frameTimestamps.push(currentTime)
while (frameTimestamps.size >= frameRateWindow) frameTimestamps.removeLast()
val timestampFirst = frameTimestamps.peekFirst() ?: currentTime
val timestampLast = frameTimestamps.peekLast() ?: currentTime
framesPerSecond = 1.0 / ((timestampFirst - timestampLast) /
frameTimestamps.size.coerceAtLeast(1).toDouble()) * 1000.0
lastAnalyzedTimestamp = frameTimestamps.first
val buffer = image.planes[0].buffer
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
listeners.forEach { it(luma) }
image.close()
}
}
companion object {
private const val TAG = "CameraXBasic"
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val PHOTO_EXTENSION = ".jpg"
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
private fun createFile(baseFolder: File, format: String, extension: String) =
File(baseFolder, SimpleDateFormat(format, Locale.US)
.format(System.currentTimeMillis()) + extension)
}
}
|