Google Android
在Camera2
的基础上再开发了CameraX
,用于解决以往Camera/Camera2
装备冗杂的问题,力求做到开发者的开箱即用体验,本篇在Android Codelab
的基础上,加入了少量注释,力求代码的明晰易懂
Android官方供给的相关攻略
- 从 Camera1 搬迁到 CameraX 的搬迁攻略
- CameraX概览
- CameraX运用示例
- CameraX Codelab
准备工作
创建一个新项目
- 运用
Android Studio
菜单,新建项目并在收到系统提示时挑选Empty Activity
(空Activity
) - 下一步,将运用命名为
CameraX App
。保证将言语设置为Kotlin
、将最低API
等级设为21
(对于CameraX
,这是所需的最低等级),保证AndroidX
组件相关设置翻开
增加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
,用以表明相机可以是前置摄像头或后置摄像头
假如您运用不带
.any
的android.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
代替,在github
上Android
团队发布的用例中,现已采用了更轻量级的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
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))
}
- 运转,此刻的预览效果
拍照照片并保存
-
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()
}
}