我的编程空间,编程开发者的网络收藏夹
学习永远不晚

十分钟实现 Android Camera2 相机预览

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

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

十分钟实现 Android Camera2 相机预览

1. 前言

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

2. Camera2 API 概述

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

  • CameraManager : Camera设备的管理类,通过该对象可以查询设备的Camera设备信息,得到CameraDevice对象
  • CameraDevice:CameraDevice提供了Camera设备相关的一系列固定参数,例如基础的设置和输出格式等。这些信息包含在CameraCharacteristic类中,可以通过getCameraCharacteristics(String)获得该类对象。
  • CaptureSession : 在Camera API中,如何需要从Camera设备中获取视频或图片流,首先需要使用输出的SurfaceCameraDevice创建一个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// 获取所有摄像头的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

十分钟实现 Android Camera2 相机预览

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android实现Camera2预览和拍照效果

简介网上对于 Camera2 的介绍有很多,在 Github 上也有很多关于 Camera2 的封装库,但是对于那些库,封装性太强,有时候我们仅仅是需要个简简单单的拍照功能而已,因此,自定义一个 Camera 使之变得轻量级那是非常重要的了
2023-05-30

Android项目中如何实现自定义相机预览界面

Android项目中如何实现自定义相机预览界面?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。官方文档:public static void setCameraD
2023-05-31

怎么在Android 中利用camera2 API 实现一个相机功能

怎么在Android 中利用camera2 API 实现一个相机功能?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。流程因为 camera2 提供的接口比较多,虽然很灵活,但是也
2023-05-31

Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作

1. CameraX架构 看官方文档 CameraX架构 有如下这一段话 使用CameraX,借助名为"用例"的抽象概念与设备的相机进行交互。 预览 : 接受用于显示预览的Surface,例如PreviewView图片分析 : 为分析 (例
2023-08-20

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录