文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

十分钟实现 Android Camera2 相机预览

2023-10-26 15:34

关注

1. 前言

因为工作中要使用Android Camera2 API,但因为Camera2比较复杂,网上资料也比较乱,有一定入门门槛,所以花了几天时间系统研究了下,并在CSDN上记录了下,希望能帮助到更多的小伙伴。

2. Camera2 API 概述

Camera2 API的包名是android.hardware.camera2,是Android 5.0后推出的一套调用摄像头设备的接口,用来替换原有的CameraCamera2 API采用管道式的设计,使数据流从摄像头流向Surface,使用Camera2 API实现拍照录制视频功能时,主要涉及到以下几个类:

一个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// 获取所有摄像头的CameraIDfun 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"   }}

还有一个简易的判断方式,一般情况下cameraId0是后摄,cameraId1是前摄。

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

<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 ,前摄 : 1private val cameraId = "0"private val TAG = CameraActivity::class.java.simpleNameprivate lateinit var cameraDevice: CameraDeviceprivate 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 = camerastartPreview()        }        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 )    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        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
<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">        <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.Pointimport android.hardware.camera2.CameraCharacteristicsimport android.hardware.camera2.params.StreamConfigurationMapimport android.util.Sizeimport android.view.Displayimport java.lang.Math.maximport java.lang.Math.minclass 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})"}val SIZE_1080P: SmartSize = SmartSize(1920, 1080)fun getDisplaySmartSize(display: Display): SmartSize {    val outPoint = Point()    display.getRealSize(outPoint)    return SmartSize(outPoint.x, outPoint.y)}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)

来源地址:https://blog.csdn.net/EthanCo/article/details/131371887

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯