我在 googleIO 前查看演讲主题,看到了有一篇标题是关于 cameraX ,当时在 android 官网没有搜索到教程

在 flutter web 基本体验完毕后, 再去搜索果然从官网查看到了 cameraX 的相关介绍

官网链接 官方使用说明

目录

介绍

CameraX 是 Jetpack 的一部分, 旨在帮助更好更简单的使用照相机

最低支持的 API 等级是 API 21(5.0)

开发环境

我当前的开发环境是

MacOS 10.13.6
Android Studio 3.4 小米 8 MIUI 10.3 稳定版 10.3.2.0(android 9.0)

最低支持 官方说明为 AndroidStudio 3.3 API 21+的设备

编码准备

新建项目

修改这两项

20190514135542.png

最低 21, 使用 androidX

添加依赖

在 app 级别的 build.gradle 中添加如下依赖

这个版本当前还是 alpha 版本,后续可能会升级为正式版, 可以查看mvn 仓库中的版本号

dependencies {
    def camerax_version = "1.0.0-alpha01"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
}

尝试运行项目

运行项目,我这里可以成功跑起来,说明依赖添加是成功的

device-2019-05-14-141300.png

编码

修改 xml 文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <TextureView
            android:id="@+id/view_finder"
            android:layout_width="640px"
            android:layout_height="640px"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

使用TextureView作为显示的 View

申请权限

添加到 manifest

<uses-permission android:name="android.permission.CAMERA" />

申请动态访问权限,这一步可以借助第三方插件,也可以自己写

截至目前为止,代码如下

package top.kikt.camerax.usage

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.TextureView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*

private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

class MainActivity : AppCompatActivity(), LifecycleOwner {

    private lateinit var viewFinder: TextureView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewFinder = view_finder

        // Request camera permissions
        if (allPermissionsGranted()) {
            viewFinder.post { startCamera() }
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            )
        }

        // Every time the provided texture view changes, recompute layout
        viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            updateTransform()
        }
    }


    private fun startCamera() {

    }

    private fun updateTransform() {}

    /**
     * Process result from permission request dialog box, has the request
     * been granted? If yes, start Camera. Otherwise display a toast
     */
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                Toast.makeText(
                    this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT
                ).show()
                finish()
            }
        }
    }

    /**
     * Check if all permission specified in the manifest have been granted
     */
    private fun allPermissionsGranted(): Boolean {
        for (permission in REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(
                    this, permission
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                return false
            }
        }
        return true
    }
}

实现照相的功能

实现 startCamera 的逻辑

    private fun startCamera() {

        // Create configuration object for the viewfinder use case
        val previewConfig = PreviewConfig.Builder().apply {
            setTargetAspectRatio(Rational(1, 1))
            setTargetResolution(Size(640, 640))
        }.build()

        // Build the viewfinder use case
        val preview = Preview(previewConfig)

        // Every time the viewfinder is updated, recompute layout
        preview.setOnPreviewOutputUpdateListener {

            // To update the SurfaceTexture, we have to remove it and re-add it
            val parent = viewFinder.parent as ViewGroup
            parent.removeView(viewFinder)
            parent.addView(viewFinder, 0)

            viewFinder.surfaceTexture = it.surfaceTexture
            updateTransform()
        }

        // Bind use cases to lifecycle
        // If Android Studio complains about "this" being not a LifecycleOwner
        // try rebuilding the project or updating the appcompat dependency to
        // version 1.1.0 or higher.
        CameraX.bindToLifecycle(this, preview)
    }

    private fun updateTransform() {
        val matrix = Matrix()

        // 计算中心
        val centerX = viewFinder.width / 2f
        val centerY = viewFinder.height / 2f

        // 纠正屏幕方向的错误
        val rotationDegrees = when (viewFinder.display.rotation) {
            Surface.ROTATION_0 -> 0
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            else -> return
        }
        matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)

        // 把纠正错误后的矩阵传给viewFinder
        viewFinder.setTransform(matrix)
    }

可能遇到的错误:

java.lang.NoSuchMethodError: No super method getLifecycle()Landroidx/lifecycle/Lifecycle; in class Landroidx/core/app/ComponentActivity; or its super classes (declaration of 'androidx.core.app.ComponentActivity' appears in ........

Stack Overflow上找到了一个解释

简单来说, 修改一下依赖

//    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.appcompat:appcompat:1.1.0-alpha05'

因为 1.0.2 版本中还没有实现 LifecycleOwner 的接口, 然后就可以删掉 Activity 声明上的 LifecycleOwner 了

1.1.0-alpha05 版本的继承关系图如下:

然后目前的预览是这样的:

31698213-5C2F-4B1C-BD70-2C51628ABF6A.png

预览这一步就完成了,相对于以前的 api 来说, 真实的编码量很小

拍照

接着就是获取当前的画面了

先添加一个按钮

<ImageButton
        android:id="@+id/capture_button"
        android:layout_width="72dp"
        android:layout_height="72dp"
        android:layout_margin="24dp"
        app:srcCompat="@android:drawable/ic_menu_camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

修改 startCamera 方法

添加一段代码

     // 添加拍照的代码, 这里和预览的相同
        val imageCaptureConfig = ImageCaptureConfig.Builder()
            .apply {
                setTargetAspectRatio(Rational(1, 1))
                // setTargetResolution(Size(640, 640)) // 设置这个,但实际获取则会根据这个有所不同,根据注释说明, 会是与目标更接近的一个分辨率, 但是可能会因为设备的不同而造成可能崩溃的问题, 不确定就不设置
                setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
            }.build()

        val imageCapture = ImageCapture(imageCaptureConfig)
        // 设置一个点击事件
        capture_button.setOnClickListener {
            val file = File(
                externalMediaDirs.first(),
                "${System.currentTimeMillis()}.jpg"
            )

            // 捕捉图片
            imageCapture.takePicture(file, object : ImageCapture.OnImageSavedListener {
                override fun onError(
                    error: ImageCapture.UseCaseError,
                    message: String, exc: Throwable?
                ) {
                    val msg = "Photo capture failed: $message"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.e("CameraXApp", msg)
                    exc?.printStackTrace()
                }

                override fun onImageSaved(file: File) {
                    val msg = "Photo capture succeeded: ${file.absolutePath}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d("CameraXApp", msg)
                }
            })
        }

别忘了修改这里

CameraX.bindToLifecycle(this, preview, imageCapture)

然后点击拍照按钮

2019-05-14 15:20:50.839 13521-13521/top.kikt.camerax.usage D/CameraXApp: Photo capture succeeded: /storage/emulated/0/Android/media/top.kikt.camerax.usage/1557818450473.jpg

可以看到我们成功的拍了一张照片

使用 adb 命令导出这个图片, 没有配置的话建议你去配置一下

adb pull /storage/emulated/0/Android/media/top.kikt.camerax.usage/1557818450473.jpg
open 1557818450473.jpg

20190514152242.png

分析器

新建一个分析器类,需要继承 ImageAnalysis.Analyzer

备注: 这个代码来自于官方示例(开篇那个连接)

目的是记录平均亮度


import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit

/// create 2019-05-14 by cai
class LuminosityAnalyzer : ImageAnalysis.Analyzer {
    private var lastAnalyzedTimestamp = 0L

    /**
     * Helper extension function used to extract a byte array from an
     * image plane buffer
     */
    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()    // Rewind the buffer to zero
        val data = ByteArray(remaining())
        get(data)   // Copy the buffer into a byte array
        return data // Return the byte array
    }

    override fun analyze(image: ImageProxy, rotationDegrees: Int) {
        val currentTimestamp = System.currentTimeMillis()
        // Calculate the average luma no more often than every second
        if (currentTimestamp - lastAnalyzedTimestamp >=
            TimeUnit.SECONDS.toMillis(1)
        ) {
            // Since format in ImageAnalysis is YUV, image.planes[0]
            // contains the Y (luminance) plane
            val buffer = image.planes[0].buffer
            // Extract image data from callback object
            val data = buffer.toByteArray()
            // Convert the data into an array of pixel values
            val pixels = data.map { it.toInt() and 0xFF }
            // Compute average luminance for the image
            val luma = pixels.average()
            // Log the new luma value
            Log.d("CameraXApp", "Average luminosity: $luma")
            // Update timestamp of last analyzed frame
            lastAnalyzedTimestamp = currentTimestamp
        }
    }
}

还是回到startCamera方法


    /// 统计配置
    val analyzerConfig = ImageAnalysisConfig.Builder().apply {
        // Use a worker thread for image analysis to prevent glitches
        val analyzerThread = HandlerThread(
            "LuminosityAnalysis"
        ).apply { start() }
        setCallbackHandler(Handler(analyzerThread.looper))
        // In our analysis, we care more about the latest image than
        // analyzing *every* image
        setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
    }.build()

    // 设置
    val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
        analyzer = LuminosityAnalyzer()
    }

别忘了绑定生命周期

    // 绑定生命周期和CameraX , 这里第二步修改的时候别忘了把imageCapture 也一起绑定上
    // 同理,第三步的时候需要绑上分析器
    CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)

接着重新运行代码,随着预览的图像不同会呈现一个平均亮度

2019-05-14 15:37:32.883 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 136.85490234375
2019-05-14 15:37:33.889 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 118.26471354166667
2019-05-14 15:37:34.883 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 137.15953450520834
2019-05-14 15:37:35.934 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 136.98435221354165
2019-05-14 15:37:36.929 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 137.244296875
2019-05-14 15:37:37.964 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 136.60428059895833
2019-05-14 15:37:38.984 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 136.93064127604165
2019-05-14 15:37:39.967 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 125.22169921875
2019-05-14 15:37:41.001 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 115.343046875
2019-05-14 15:37:42.045 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 107.64242838541666
2019-05-14 15:37:43.061 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 76.732939453125
2019-05-14 15:37:44.082 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 112.82620442708334
2019-05-14 15:37:45.105 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 116.12317057291666
2019-05-14 15:37:46.086 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 115.94841796875
2019-05-14 15:37:47.142 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 117.0526171875
2019-05-14 15:37:48.165 14412-14435/top.kikt.camerax.usage D/CameraXApp: Average luminosity: 114.96781901041666

编码结束

到这一步为止, 编码的过程就结束了

进阶探索

通常来说,相机现在会结合很多其他的用途

  1. 摄像
  2. 直播视频上传(视频通话)
  3. 图像识别
  4. 增强现实
  5. 其他…

别看我说的很热闹,但是让我结合这几个方向来写的话,篇幅不允许是一点, 而且这些东西每一行都是能写一整个系列文章的…(我才不会说是我不会做呢, 哼 😒)

还记得 ImageAnalysis.Analyzer 这个类吗, 这个类的analyze方法会回调一些信息

20190514154722.png

简单来说, 会回调一些 image 的信息和角度

ImageProxy 中包含很多的信息

常用的有:

getFormat: 视频格式 ,具体查看文档 20190514162133.png

一般来说都应该是YUV_420_888

getWidth: 宽度

getHeight: 高度

getTimeStamp: 据说是纳秒单位, 和设备的时间基有关, 我是没看懂什么意思 😁

getPlanes: 视频数据, 类型是PlaneProxy[] kotlin 对应:Array<PlaneProxy>, 这东西的 size 是根据 getFormat 的格式决定的

关于 YUV_420_888 这部分可以查看Android-Image 类浅析-结合 YUV_420_888Android: YUV_420_888 编码 Image 转换为 I420 和 NV21 格式 byte 数组

然后通过解析数据封装成需要的格式就可以了

后记

简单来说, CameraX 的 api 比 Camera2 和 Camera 看起来都要好很多

项目地址: https://github.com/CaiJingLong/android-cameraX-example

以上