Google AndroidCamera2的基础上再开发了CameraX,用于解决以往Camera/Camera2装备冗杂的问题,力求做到开发者的开箱即用体验,本篇在Android Codelab的基础上,加入了少量注释,力求代码的明晰易懂

Android官方供给的相关攻略

  • 从 Camera1 搬迁到 CameraX 的搬迁攻略
  • CameraX概览
  • CameraX运用示例
  • CameraX Codelab

准备工作

创建一个新项目

  • 运用 Android Studio 菜单,新建项目并在收到系统提示时挑选Empty Activity(空 Activity
    CameraX 简单使用
  • 下一步,将运用命名为CameraX App。保证将言语设置为Kotlin、将最低API等级设为21(对于CameraX,这是所需的最低等级),保证AndroidX组件相关设置翻开

CameraX 简单使用

增加Gradle依靠项

  • 翻开build.gradle(Module: app)文件并将CameraX依靠项增加到运用Gradle文件中的dependencies部本分:
// CameraX core library using the camera2 implementation
def camerax_version = "1.3.0-beta01"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
// If you want to additionally use the CameraX Lifecycle library
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
// If you want to additionally use the CameraX VideoCapture library
implementation("androidx.camera:camera-video:${camerax_version}")
// If you want to additionally use the CameraX View class
implementation("androidx.camera:camera-view:${camerax_version}")
// If you want to additionally add CameraX ML Kit Vision Integration
implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
// If you want to additionally use the CameraX Extensions library
implementation("androidx.camera:camera-extensions:${camerax_version}")
  • CameraX需求用到Java 8中的一些方法,因而咱们需求对编译选项进行相应设置。 在android块结尾,紧跟buildTypes的位置增加以下内容:
compileOptions {
  sourceCompatibility JavaVersion.VERSION_1_8
  targetCompatibility JavaVersion.VERSION_1_8
}
  • 在弹出的消息中挑选Sync Now,把在build gradle中装备的更改进行同步
  • 装备成功的build.gradle应该如下所示:
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}
android {
    namespace 'com.mobilescanner.cameraxdemo'
    compileSdk 33
    defaultConfig {
        applicationId "com.mobilescanner.cameraxdemo"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
dependencies {
    // CameraX core library using the camera2 implementation
    def camerax_version = "1.3.0-beta01"
    // The following line is optional, as the core library is included indirectly by camera-camera2
    implementation("androidx.camera:camera-core:${camerax_version}")
    implementation("androidx.camera:camera-camera2:${camerax_version}")
    // If you want to additionally use the CameraX Lifecycle library
    implementation("androidx.camera:camera-lifecycle:${camerax_version}")
    // If you want to additionally use the CameraX VideoCapture library
    implementation("androidx.camera:camera-video:${camerax_version}")
    // If you want to additionally use the CameraX View class
    implementation("androidx.camera:camera-view:${camerax_version}")
    // If you want to additionally add CameraX ML Kit Vision Integration
    implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
    // If you want to additionally use the CameraX Extensions library
    implementation("androidx.camera:camera-extensions:${camerax_version}")
    implementation 'androidx.core:core-ktx:1.10.1'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'android.arch.lifecycle:livedata:1.1.1'
    implementation 'android.arch.lifecycle:viewmodel:1.1.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

AndroidManifest装备

  • 增加用于保证设备配备有相机的android.hardware.camera.any。指定.any,用以表明相机可以是前置摄像头或后置摄像头

假如您运用不带.anyandroid.hardware.camera,则在您运用没有后置摄像头的设备(例如,大多数 Chromebook)的情况下,此类将无法工作。在第二行中增加对于该相机的拜访权限。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- Declare features -->
    <uses-feature android:name="android.hardware.camera.any" />
    <!-- Declare permissions -->
    <uses-permission android:name="android.permission.CAMERA" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.CameraXDemo"
        tools:targetApi="31">
        <!--下文创建MainActivity时会主动生成 -->
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

留意

  • Codelab编写教程时采用了kotlin-android-extensions,但现在其现已被弃用,故文中需求运用控件处都运用findViewById代替,在githubAndroid团队发布的用例中,现已采用了更轻量级ViewBinding
  • 假如Sync Now过慢,考虑把源更改为阿里源,在目前该文章编写版别的Android Studio(2022.2.1)中,仓库装备源的设置已更改至settings.gradle,装备成功文件如下所示:
pluginManagement {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/public' }
        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
        maven { url 'https://maven.aliyun.com/repository/central' }
        maven { url 'https://maven.aliyun.com/repository/google' }
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url 'https://maven.aliyun.com/repository/public' }
        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
        maven { url 'https://maven.aliyun.com/repository/central' }
        maven { url 'https://maven.aliyun.com/repository/google' }
        google()
        mavenCentral()
    }
}
rootProject.name = "CameraXDemo"
include ':app'

项目实现

创建取景器布局

  • 创建一个MainActivity,并在layout/activity_main.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">
 <Button
   android:id="@+id/camera_capture_button"
   android:layout_width="100dp"
   android:layout_height="100dp"
   android:layout_marginBottom="50dp"
   android:scaleType="fitCenter"
   android:text="Take Photo"
   app:layout_constraintLeft_toLeftOf="parent"
   app:layout_constraintRight_toRightOf="parent"
   app:layout_constraintBottom_toBottomOf="parent"
   android:elevation="2dp" />
 <androidx.camera.view.PreviewView
   android:id="@+id/viewFinder"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity的根本实现

  • 界说所需常量
package com.example.cameraxdemo
import android.Manifest
import android.content.pm.PackageManager
import android.icu.text.SimpleDateFormat
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.io.File
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
    /**
     * TAG:后续编写中需求运用Log.e的Tag
     * FILENAME_FORMAT:该示例中保存图片文件的文件名为时刻戳,该变量为时刻戳界说方式
     * REQUEST_CODE_PERMISSIONS:恳求相机运用权限时的恳求码
     * REQUIRED_PERMISSIONS:需求恳求运用的权限
     */
    companion object {
        private const val TAG = "CameraXBasic"
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
        /**
         * 留意在此处Manifest.permission.CAMERA可能会出现找不到的情况
         * 检查你运用的是否为Android包的Manifest而不是你本项目里的Manifest(删了在Android Studio的提示下重打)
         */
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
  • 恳求权限
...
class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, 
                REQUIRED_PERMISSIONS,
                REQUEST_CODE_PERMISSIONS
            )
        }
    }
    private fun startCamera(){ }
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
    override fun onRequestPermissionsResult(
       requestCode: Int, permissions: Array<String>, grantResults:IntArray) {
       //检查恳求代码是否正确;假如此代码不正确,则将其忽略。
       if (requestCode == REQUEST_CODE_PERMISSIONS) {
           //假如权限现已被颁发,调用相机
           if (allPermissionsGranted()) {
               startCamera()
           } else {
           //权限未颁发,告知用户权限授权不成功
               Toast.makeText(
                   this,
                   "Permissions not granted by the user.",
                   Toast.LENGTH_SHORT).show()
               finish()
           }
       }
    }
}
  • 初始化图片输出文件夹
...
class MainActivity : AppCompatActivity() {
    ...
    private lateinit var outputDirectory: File
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        outputDirectory = getOutputDirectory()
    }
    private fun getOutputDirectory(): File {
        val mediaDir = externalMediaDirs.firstOrNull()?.let {
            File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
        return if (mediaDir != null && mediaDir.exists())
            mediaDir else filesDir
    }
}
  • 初始化线程池
...
class MainActivity : AppCompatActivity() {
    ...
    private lateinit var cameraExecutor: ExecutorService
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
         val camera_capture_button = findViewById<Button>(R.id.camera_capture_button)
        // Set up the listener for take photo button
        camera_capture_button.setOnClickListener { takePhoto() }
        cameraExecutor = Executors.newSingleThreadExecutor()
    }
    private fun takePhoto(){ }
    //生命周期结束时封闭线程池,避免内存泄漏
    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }
}
  • 运转代码,界面应如下所示

CameraX 简单使用

实现预览功用

在相机运用中,用户可凭借取景器预览他们要拍照的照片。您可以运用 CameraX Preview类实现取景器功用。如要运用Preview,您首先需求界说装备,然后运用该装备创建用例的实例。所生成的实例是您要绑定到CameraX生命周期的内容。

private fun startCamera() {
 val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
 cameraProviderFuture.addListener(Runnable {
       //创建ProcessCameraProvider的实例。
       //此实例用于将相机的生命周期绑定到lifecycler Owner。
       //因为 CameraX 具有生命周期感知才能,所以这样可以省去翻开和封闭相机的使命。
   val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
   //Preview预览类
   val preview = Preview.Builder()
     .build()
     .also {
       it.setSurfaceProvider(viewFinder.surfaceProvider())
     }
   //把后置摄像头选为默许相机
   val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
   try {
     //在从头绑定前解绑lifecycle,保证没有任何owner在此之前现已绑定
     cameraProvider.unbindAll()
     //把camera和lifecycle进行绑定
     cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
   } catch(exc: Exception) {
     Log.e(TAG, "Use case binding failed", exc)
   }
 }, ContextCompat.getMainExecutor(this))
}
  • 运转,此刻的预览效果
    CameraX 简单使用

拍照照片并保存

  • takePhoto()的实现
//此处变量声明可在onCreate()前那些变量声明处一同编写
private var imageCapture: ImageCapture? = null
private fun takePhoto() {
 //获取对ImageCapture用例的引证
   //假如用例为 null,则退出函数。
   //假如您在设置拍照图画之前点按摄影按钮,则这将为 null。
   //假如没有`return`句子,则在用例为`null`的情况下,运用会崩溃。
 val imageCapture = imageCapture ?: return
 //创建用于保存图片的时刻戳File
 val photoFile = File(
   outputDirectory,
   SimpleDateFormat(FILENAME_FORMAT, Locale.US
   ).format(System.currentTimeMillis()) + ".jpg")
 // 创建输出图片设置,包含File和META-DATA
 val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
 //设置iamgeCapture的监听,在图片被拍照完成后触发
 imageCapture.takePicture(
   outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
     override fun onError(exc: ImageCaptureException) {
       Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
     }
     override fun onImageSaved(output: ImageCapture.OutputFileResults) {
       val savedUri = Uri.fromFile(photoFile)
       val msg = "Photo capture succeeded: $savedUri"
       Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
       Log.d(TAG, msg)
     }
   })
}
  • startCamera()中保存图片设置
private fun startCamera() {
 val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
 cameraProviderFuture.addListener(Runnable {
      ...
       //初始化imageCaptrue
       imageCapture = ImageCapture.Builder().build()
   //把后置摄像头选为默许相机
   val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
   try {
     //在从头绑定前解绑lifecycle,保证没有任何owner在此之前现已绑定
     cameraProvider.unbindAll()
     //把camera和lifecycle进行绑定
           //把camera和imageCaptrue
     cameraProvider.bindToLifecycle(
       this, cameraSelector, preview,imageCapture
               )
   } catch(exc: Exception) {
     Log.e(TAG, "Use case binding failed", exc)
   }
 }, ContextCompat.getMainExecutor(this))
}
  • 运转运用,即可正常拍照照片并保存

完整代码

package com.mobilescanner.cameraxdemo
import android.Manifest
import android.content.pm.PackageManager
import android.icu.text.SimpleDateFormat
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.io.File
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
typealias LumaListener = (luma: Double) -> Unit
class MainActivity : AppCompatActivity() {
    /**
     * TAG:后续编写中需求运用Log.e的Tag
     * FILENAME_FORMAT:该示例中保存图片文件的文件名为时刻戳,该变量为时刻戳界说方式
     * REQUEST_CODE_PERMISSIONS:恳求相机运用权限时的恳求码
     * REQUIRED_PERMISSIONS:需求恳求运用的权限
     */
    companion object {
        private const val TAG = "CameraXBasic"
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
        /**
         * 留意在此处Manifest.permission.CAMERA可能会出现找不到的情况
         * 检查你运用的是否为Android包的Manifest而不是你本项目里的Manifest(删了在Android Studio的提示下重打)
         */
    }
    private lateinit var outputDirectory: File
    private lateinit var cameraExecutor: ExecutorService
    private var imageCapture: ImageCapture? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }
        val camera_capture_button = findViewById<Button>(R.id.camera_capture_button)
        // Set up the listener for take photo button
        camera_capture_button.setOnClickListener { takePhoto() }
        outputDirectory = getOutputDirectory()
        cameraExecutor = Executors.newSingleThreadExecutor()
    }
    private fun getOutputDirectory(): File {
        val mediaDir = externalMediaDirs.firstOrNull()?.let {
            File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
        return if (mediaDir != null && mediaDir.exists())
            mediaDir else filesDir
    }
    private fun takePhoto() {
        // Get a stable reference of the modifiable image capture use case
        val imageCapture = imageCapture ?: return
        // Create time-stamped output file to hold the image
        val photoFile = File(
            outputDirectory,
            SimpleDateFormat(FILENAME_FORMAT, Locale.US
            ).format(System.currentTimeMillis()) + ".jpg")
        // Create output options object which contains file + metadata
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
        // Set up image capture listener, which is triggered after photo has
        // been taken
        imageCapture.takePicture(
            outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                }
                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val savedUri = Uri.fromFile(photoFile)
                    val msg = "Photo capture succeeded: $savedUri"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)
                }
            })
    }
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val viewFinder = findViewById<PreviewView>(R.id.viewFinder)
        cameraProviderFuture.addListener(Runnable {
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
            // Preview
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewFinder.surfaceProvider)
                }
            imageCapture = ImageCapture.Builder()
                .build()
            // Select back camera as a default
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()
                // Bind use cases to camera
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview,imageCapture)
            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }
}