1. 前言
因为工作中要使用Android Camera2 API
,但因为Camera2
比较复杂,网上资料也比较乱,有一定入门门槛,所以花了几天时间系统研究了下,并在CSDN
上记录了下,希望能帮助到更多的小伙伴。
2. Camera2 API 概述
Camera2 API
的包名是android.hardware.camera2
,是Android 5.0
后推出的一套调用摄像头设备的接口,用来替换原有的Camera
。Camera2 API
采用管道式的设计,使数据流从摄像头流向Surface
,使用Camera2 API
实现拍照录制视频功能时,主要涉及到以下几个类:
CameraManager
:Camera
设备的管理类,通过该对象可以查询设备的Camera
设备信息,得到CameraDevice
对象CameraDevice
:CameraDevice
提供了Camera
设备相关的一系列固定参数,例如基础的设置和输出格式等。这些信息包含在CameraCharacteristic
类中,可以通过getCameraCharacteristics(String)
获得该类对象。CaptureSession
: 在Camera API
中,如何需要从Camera
设备中获取视频或图片流,首先需要使用输出的Surface
和CameraDevice
创建一个CameraCaptureSession
CaptureRequest
: 该类中定义了一个Camera
设备获取帧数据所需要的参数,可以通过CameraDevice
的工厂方法创建一个Request Builder
,用于获取CaptureRequest
CaptureResult
: 当处理完一个请求后,会返回一个TotalCaptureResult
对象,其中包含Camera
设备执行该次Request
所使用的参数以及自身状态。
一个Android
设备可以有多个摄像头。每个摄像头都是一个摄像头设备,摄像头设备可以同时输出多个流。
3. 前置设置
3.1 添加权限
在AndroidManifest.xml
中声明权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
3.2 申请权限
ActivityCompat.requestPermissions(
this@MainActivity,
arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO
), 123
)
4. 获取相机列表
4.1 获取摄像头列表
获取摄像头列表需要使用到CameraManager
,通过cameraManager.cameraIdList
可以获取到摄像头列表
private val cameraManager =
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
// 获取所有摄像头的CameraID
fun getCameraIds(): Array<String> {
return cameraManager.cameraIdList
}
4.2 判断 前/后 摄像头
通过该方法可以获取摄像头的方位,判定是前摄还是后摄
/**
* 获取摄像头方向
*/
fun getCameraOrientationString(cameraId: String): String {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING)!!
return when (lensFacing) {
CameraCharacteristics.LENS_FACING_BACK -> "后摄(Back)"
CameraCharacteristics.LENS_FACING_FRONT -> "前摄(Front)"
CameraCharacteristics.LENS_FACING_EXTERNAL -> "外置(External)"
else -> "Unknown"
}
}
还有一个简易的判断方式,一般情况下
cameraId
为0
是后摄,cameraId
为1
是前摄。
4.3 获取一下试试
我们来获取一下试试
val cameraIds = viewModel.getCameraIds()
cameraIds.forEach{ cameraId ->
val orientation = viewModel.getCameraOrientationString(cameraId)
Log.i(TAG,"cameraId : $cameraId - $orientation")
}
运行后可以发现打印了日志
cameraId : 0 - 后摄(Back)
cameraId : 1 - 前摄(Front)
5. 实现相机预览
5.1 修改布局
来修改一下XML
布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/btn_take_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_marginBottom="64dp"
android:text="拍照"/>
</FrameLayout>
5.2 声明相机参数和成员变量
//后摄 : 0 ,前摄 : 1
private val cameraId = "0"
private val TAG = CameraActivity::class.java.simpleName
private lateinit var cameraDevice: CameraDevice
private val cameraThread = HandlerThread("CameraThread").apply { start() }
private val cameraHandler = Handler(cameraThread.looper)
private val cameraManager: CameraManager by lazy {
getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
private val characteristics: CameraCharacteristics by lazy {
cameraManager.getCameraCharacteristics(cameraId)
}
private lateinit var session: CameraCaptureSession
5.3 添加SurfaceView回调
添加SurfaceView
回调,并在SurfaceView
创建的时候,去初始化相机
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder,format: Int, width: Int,height: Int) = Unit
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
override fun surfaceCreated(holder: SurfaceHolder) {
//为了确保设置了大小,需要在主线程中初始化camera
binding.root.post {
openCamera(cameraId)
}
}
})
}
5.4 打开相机
@SuppressLint("MissingPermission")
private fun openCamera(cameraId: String) {
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
startPreview()
}
override fun onDisconnected(camera: CameraDevice) {
this@CameraActivity.finish()
}
override fun onError(camera: CameraDevice, error: Int) {
Toast.makeText(application, "openCamera Failed:$error", Toast.LENGTH_SHORT).show()
}
}, cameraHandler)
}
5.5 开始预览
private fun startPreview() {
//因为摄像头设备可以同时输出多个流,所以可以传入多个surface
val targets = listOf(binding.surfaceView.holder.surface /*,这里可以传入多个surface*/)
cameraDevice.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {
override fun onConfigured(captureSession: CameraCaptureSession) {
//赋值session
session = captureSession
val captureRequest = cameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
).apply { addTarget(binding.surfaceView.holder.surface) }
//这将不断地实时发送视频流,直到会话断开或调用session.stoprepeat()
session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Toast.makeText(application,"session configuration failed",Toast.LENGTH_SHORT).show()
}
}, cameraHandler)
}
5.6 来看下效果
5.7 修正拉伸形变
5.7.1 新建AutoFitSurfaceView
新建AutoFitSurfaceView
继承自SurfaceView
,这个类可以调整为我们指定的宽高比,在显示画面的时候进行中心裁剪。
class AutoFitSurfaceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : SurfaceView(context, attrs, defStyle) {
private var aspectRatio = 0f
/**
* 设置此视图的宽高比。视图的大小将基于从参数中计算的比率来测量。
*
* @param width 相机水平分辨率
* @param height 相机垂直分辨率
*/
fun setAspectRatio(width: Int, height: Int) {
require(width > 0 && height > 0) { "Size cannot be negative" }
aspectRatio = width.toFloat() / height.toFloat()
holder.setFixedSize(width, height)
requestLayout()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
if (aspectRatio == 0f) {
setMeasuredDimension(width, height)
} else {
// Performs center-crop transformation of the camera frames
val newWidth: Int
val newHeight: Int
val actualRatio = if (width > height) aspectRatio else 1f / aspectRatio
if (width < height * actualRatio) {
newHeight = height
newWidth = (height * actualRatio).roundToInt()
} else {
newWidth = width
newHeight = (width / actualRatio).roundToInt()
}
Log.d(TAG, "Measured dimensions set: $newWidth x $newHeight")
setMeasuredDimension(newWidth, newHeight)
}
}
companion object {
private val TAG = AutoFitSurfaceView::class.java.simpleName
}
}
5.7.2 XML
布局中将SurfaceView
替换为AutoFitSurfaceView
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!--<SurfaceView-->
<com.heiko.mycamera2test.AutoFitSurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="match_parent" />
<Button
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/btn_take_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_marginBottom="64dp"
android:text="拍照"/>
</FrameLayout>
注意这里根布局不能使用
ConstraintLayout
,否则宽高比还是会出现问题
5.7.3 获取最大支持的预览大小
新建SmartSize
类,这个类通过比较显示的SurfaceView
和摄像头支持的分辨率,匹配出最大支持的预览大小
import android.graphics.Point
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.params.StreamConfigurationMap
import android.util.Size
import android.view.Display
import java.lang.Math.max
import java.lang.Math.min
/** Helper class used to pre-compute shortest and longest sides of a [Size] */
class SmartSize(width: Int, height: Int) {
var size = Size(width, height)
var long = max(size.width, size.height)
var short = min(size.width, size.height)
override fun toString() = "SmartSize(${long}x${short})"
}
/** Standard High Definition size for pictures and video */
val SIZE_1080P: SmartSize = SmartSize(1920, 1080)
/** Returns a [SmartSize] object for the given [Display] */
fun getDisplaySmartSize(display: Display): SmartSize {
val outPoint = Point()
display.getRealSize(outPoint)
return SmartSize(outPoint.x, outPoint.y)
}
/**
* Returns the largest available PREVIEW size. For more information, see:
* https://d.android.com/reference/android/hardware/camera2/CameraDevice and
* https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap
*/
fun <T>getPreviewOutputSize(
display: Display,
characteristics: CameraCharacteristics,
targetClass: Class<T>,
format: Int? = null
): Size {
// Find which is smaller: screen or 1080p
val screenSize = getDisplaySmartSize(display)
val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short
val maxSize = if (hdScreen) SIZE_1080P else screenSize
// If image format is provided, use it to determine supported sizes; else use target class
val config = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
if (format == null)
assert(StreamConfigurationMap.isOutputSupportedFor(targetClass))
else
assert(config.isOutputSupportedFor(format))
val allSizes = if (format == null)
config.getOutputSizes(targetClass) else config.getOutputSizes(format)
// Get available sizes and sort them by area from largest to smallest
val validSizes = allSizes
.sortedWith(compareBy { it.height * it.width })
.map { SmartSize(it.width, it.height) }.reversed()
// Then, get the largest output size that is smaller or equal than our max size
return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size
}
5.7.4 设置宽高比
我们在原本调用openCamera()
方法之前的地方,先去设置一下宽高比setAspectRatio()
binding.surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
//...省略了代码....
override fun surfaceCreated(holder: SurfaceHolder) {
//设置宽高比
setAspectRatio()
//为了确保设置了大小,需要在主线程中初始化camera
binding.root.post {
openCamera2(cameraId)
}
}
})
private fun setAspectRatio() {
val previewSize = getPreviewOutputSize(
binding.surfaceView.display,
characteristics,
SurfaceHolder::class.java
)
Log.d(TAG, "Selected preview size: $previewSize")
binding.surfaceView.setAspectRatio(previewSize.width, previewSize.height)
}
5.7.5 再次运行预览
可以看到,现在比例显示正常了
5.8 销毁相机
在Activity
销毁的时候,我们也要去销毁相机,代码如下
override fun onStop() {
super.onStop()
try {
cameraDevice.close()
} catch (exc: Throwable) {
Log.e(TAG, "Error closing camera", exc)
}
}
override fun onDestroy() {
super.onDestroy()
cameraThread.quitSafely()
//imageReaderThread.quitSafely()
}
6. 其他
6.1 本文源码下载
下载地址 : Android Camera2 Demo - 实现相机预览、拍照、录制视频功能
6.2 Android Camera2 系列
更多Camera2相关文章,请看
十分钟实现 Android Camera2 相机预览_氦客的博客-CSDN博客
十分钟实现 Android Camera2 相机拍照_氦客的博客-CSDN博客
十分钟实现 Android Camera2 视频录制_氦客的博客-CSDN博客
6.3 Android 相机相关文章
Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作_氦客的博客-CSDN博客
Android 从零开发一个简易的相机App_android开发简易app_氦客的博客-CSDN博客
6.4 参考
本文参考文章
[Android进阶] 使用Camera2 API实现一个相机预览页面
实现预览 | Android 开发者 | Android Developers (google.cn)