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创建一个CameraCaptureSessionCaptureRequest : 该类中定义了一个Camera设备获取帧数据所需要的参数,可以通过CameraDevice的工厂方法创建一个Request Builder,用于获取CaptureRequestCaptureResult : 当处理完一个请求后,会返回一个TotalCaptureResult对象,其中包含Camera设备执行该次Request所使用的参数以及自身状态。

一个Android设备可以有多个摄像头。每个摄像头都是一个摄像头设备,摄像头设备可以同时输出多个流。

3. 前置设置

3.1 添加权限

在AndroidManifest.xml中声明权限

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 {

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布局

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="@color/black"

xmlns:app="http://schemas.android.com/apk/res-auto">

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" />

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="拍照"/>

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

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="@color/black"

xmlns:app="http://schemas.android.com/apk/res-auto">

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" />

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="拍照"/>

注意这里根布局不能使用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 getPreviewOutputSize(

display: Display,

characteristics: CameraCharacteristics,

targetClass: Class,

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)

参考链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。