十分钟实现 Android Camera2 相机预览

这篇具有很好参考价值的文章主要介绍了十分钟实现 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设备可以有多个摄像头。每个摄像头都是一个摄像头设备,摄像头设备可以同时输出多个流。
android camera2,android,相机,Camera2,音视频,预览

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

还有一个简易的判断方式,一般情况下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布局

<?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 来看下效果

可以看到预览画面是出来了,但是比例不对,有拉伸形变,下面我们会来解决这个问题

android camera2,android,相机,Camera2,音视频,预览

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 再次运行预览

可以看到,现在比例显示正常了
android camera2,android,相机,Camera2,音视频,预览

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://www.toymoban.com/news/detail-645134.html

到了这里,关于十分钟实现 Android Camera2 相机预览的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 还在苦恼如何开发一个Chrome插件吗?十分钟带你实现一个实用小插件

    你是否曾考虑过创建自己的 Chrome 插件,但又挠头毫无思路?那么在接下来的几分钟里,我不仅会介绍 Chrome 浏览器扩展的基本知识,还会指导你通过五个简单的步骤来制作自己的扩展。 知道怎么做吗?让我们一探究竟! 今年我们见证了人工智能能力的爆炸式增长。虽然cha

    2024年02月10日
    浏览(31)
  • 十分钟掌握 Flink CDC,实现Mysql数据增量备份到Clickhouse [纯干货,建议收藏]

    Clickhouse的优点. 真正的面向列的 DBMS ClickHouse 是一个 DBMS,而不是一个单一的数据库。它允许在运行时创建表和数据库、加载数据和运行 查询,而无需重新配置和重新启动服务器。 数据压缩 一些面向列的 DBMS(InfiniDB CE 和 MonetDB)不使用数据压缩。但是,数据压缩确实提高了

    2024年04月14日
    浏览(33)
  • 零编程经验,通过 GPT-4 十分钟开发了一个浏览器插件,并成功运行,实现了需求目标!

    大佬蓝鸟ID: sundyme 零编程经验,通过 GPT-4 十分钟开发了一个浏览器插件,并成功运行,实现了需求目标!太不可思意了,真正体会到了自然语言编程的魅力! 下一步是利用Pinterest 的 API 接口实现自动发图,已经生成好了代码和步骤(看着挺靠谱),等明天开发者权限审核下

    2023年04月08日
    浏览(30)
  • 十分钟入门Zigbee

    大部分教程通常都是已Zigbee原理开始讲解和学习,各种概念让初学者难以理解。本教程从一个小白的角度出发,入门无需任何Zigbee底层原理知识,只需要基本的MCU研发经验就可以掌握,让您快速实现zigbee组网和节点之间通信。 本教程采用泰凌微TLSR8258芯片,芯片资料链接TLS

    2023年04月09日
    浏览(44)
  • 每天十分钟学会Spark

    Spark是什么 Spark是一种基于内存的快速、通用、可拓展的大数据分析计算引擎。 Spark官网:http://spark.apache.org/ Spark的特点 1、快速   一般情况下,对于迭代次数较多的应用程序,Spark程序在内存中的运行速度是Hadoop MapReduce运行速度的100多倍,在磁盘上的运行速度是Hadoop MapRedu

    2024年03月18日
    浏览(38)
  • 十分钟掌握Java本地缓存

    —————————— Yesterday is history, tomorrow is a mystery, but today is a gift. That is why it’s called the present. —————————— 缓存是Java开发中经常用到的组件,我们会使用缓存来存储一些 不经常改变 的 热点 数据,提高系统处理效率,其根本原因在于内存和硬盘读写速度的

    2024年02月05日
    浏览(42)
  • 通俗易懂,十分钟读懂DES,详解DES加密算法原理,DES攻击手段以及3DES原理。Python DES实现源码

    DES(Data Encryption Standard)是一种 对称加密 算法。它是在20世纪70年代初期由IBM研发的。它的设计目标是提供高度的数据安全性和性能,并且能够在各种硬件和软件平台上实现。 DES使用56位的密钥和64位的明文块进行加密。DES算法的分组大小是64位,因此,如果需要加密的明文长

    2024年02月03日
    浏览(58)
  • Django入门,十分钟学会登录网页

    我们假定你已经阅读了 安装 Django。你能知道 Django 已被安装,且安装的是哪个版本,通过在命令提示行输入命令 cmd黑窗口运行,不懂cmd百度一下 如果这是你第一次使用 Django 的话,你需要一些初始化设置。也就是说,你需要用一些自动生成的代码配置一个 Django project ——

    2024年01月24日
    浏览(40)
  • 十分钟理解回归测试(Regression Testing)

    回归测试是一个系统的质量控制过程,用于验证最近对软件的更改或更新是否无意中引入了新错误或对以前的功能方面产生了负面影响(比如你在家中安装了新的空调系统,发现虽然新的空调系统可以按预期工作,但是本来亮的等却不亮了)。其主要目标是确保旨在改进的修

    2024年02月05日
    浏览(46)
  • 十分钟python入门 正则表达式

    正则常见的三种功能,它们分别是:校验数据的有效性、查找符合要求的文本以及对文本进行切割和替换等操作。 所谓元字符就是指那些在正则表达式中具有特殊意义的专用字符 元字符大致分成这几类:表示单个特殊字符的,表示空白符的,表示某个范围的,表示次数的量

    2024年02月13日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包