Hôm nay mình sẽ viết một bài về CameraX của Android Jetpack. Về cơ bản thì CameraX là một thư viện của bộ Jetpack nó giúp việc xây dựng một app về camera trở nên dễ dàng hơn, nó rút gọn code hơn so với thư viện trước đó và còn có thêm các extension cho phép thêm các hiệu ứng trên các dòng thiết bị có hỗ trợ. Ví dụ như: HDR, Beauty, chụp đêm...
CameraX structure
Các dev sử dụng CameraX để giao tiếp với máy ảnh của thiết bị thông qua một phần trừu tượng gọi là use case và một vài use case hiện có sẵn:
Preview: cho phép trên app hiển thị những gì camera đang chụp.
Image analysis: cung cấp bộ đệm có thể truy cập CPU để phân tích ảnh, dùng nhiều trong Machine Learning.
Image capture: Chụp và lưu ảnh.
Các use case này có thể được dùng song song với nhau.
Tạo một app CameraX đơn giản
Configure
Minimum API là 21 và dùng Android Stuido 3.6 trở lên.
Mở build.gradle(Module: app) thêm các dependencies bên dưới vào .
def camerax_version = "1.0.0-beta12"
// CameraX core library using camera2 implementation
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:1.0.0-alpha19"
CameraX cần một số phương thức của Java 8, vì vậy chúng ta cần đặt các tùy chọn biên dịch của mình cho phù hợp.
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
....
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
...
}
Trong file AndroidManifest.xml
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
Sau đó sync lại gradle.
Code UI đơn giản
Trong file activity_main.xml
thêm đoạn code như sau:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.jp.gyao.camerax.MainActivity">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btCamera"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="Take a Photo"
android:elevation="10dp"
android:includeFontPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
//Thẻ preview cho phép hiển thị lên màn hình điện thoại những gì camera thấy được.
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Code các chức năng chính
Trong MainActivity.kt
của mình thì trong onCreate ()
sẽ để hàm xin quyền Camera. Mặc dù ở onCreate () đã xin quyền hoàn tất nhưng máy ảnh sẽ chưa hoạt động cho đến khi ta triển khai các phương thức trong tệp.
typealias LumaListener = (luma: Double) -> Unit
class MainActivity : AppCompatActivity() {
private var imageCapture: ImageCapture? = null
private lateinit var outputDirectory: File
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Xin quyền máy ảnh
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
// Lắng nghe sự kiện chụp ảnh.
camera_capture_button.setOnClickListener { takePhoto() }
//Một biến để lưu trữ đường dẫn ảnh.
outputDirectory = getOutputDirectory()
//Tạo một luồng mới chạy độc lập.
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun takePhoto() {}
private fun startCamera() {}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
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
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
companion object {
private const val TAG = "CameraXCoBan"
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)
}
}
Implement Preview use case
Trong một ứng dụng máy ảnh, viewfinder
được sử dụng để cho phép người dùng xem trước ảnh họ sẽ chụp. Ta có thể triển khai viewfinder
bằng cách sử dụng lớp CameraX Preview.
private fun startCamera() {
// Sử dụng để ràng buộc vòng đời của máy ảnh với vòng đời của View. Điều này giúp loại bỏ nhiệm vụ mở và đóng máy ảnh vì CameraX nhận biết được vòng đời.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
// Lựa chọn mặc định dùng camera sau.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// Bên trong khối try, hãy đảm bảo không có gì liên kết với cameraProvider, sau đó liên kết cameraSelector và đối tượng Preview với cameraProvider.
try {
// Huỷ liên kết với vòng đời của View trước khi liên kết trở lại
cameraProvider.unbindAll()
// Liên kết Preview use case đến Camera.
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Liên kết thất bại", exc)
}
}, ContextCompat.getMainExecutor(this)) //Chạy trên luồng chính.
}
Implement ImageCapture use case
private fun takePhoto() {
// Đầu tiên, hãy kiểm tra ImageCapture. Nếu trường hợp sử dụng là null, thì ta thoát ra khỏi hàm.
val imageCapture = imageCapture ?: return
// Tiếp theo, tạo một tệp để lưu giữ hình ảnh. Thêm dấu thời gian để tên tệp sẽ là duy nhất.
val photoFile = File(outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis()) + ".png")
// Tạo một đối tượng OutputFileOptions. Đối tượng này là nơi ta có thể chỉ định mọi thứ về đầu ra của mình. Ở đây mình sẽ lưu vào trong tệp vừa tạo ở trên.
val outputOption = ImageCapture.OutputFileOptions.Builder(photoFile).build()
// Khi gọi hàm takePicture() trên đối tượng imageCapture. Truyền vào outputOptions, trình thực thi và một lệnh gọi lại khi ảnh được lưu.
imageCapture.takePicture(
outputOption, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
// Nếu việc chụp không bị lỗi, ảnh đã được chụp thành công! Lưu ảnh vào tệp bạn đã tạo trước đó.
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Đường dẫn ảnh: $savedUri"
Toast.makeText(this@MainActivity, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
// Nếu thất bại thì in ra log.
override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "Không lấy được ảnh: ${exception.message}", exception)
}
})
}
Tiếp đó vào trong hàmstartCamera()
thêm vào đoạn code sau:\
imageCapture = ImageCapture.Builder().build()
...
// Thêm biến imageCapture vào trong cameraProvider
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
Implement ImageAnalys use case
Để làm cho ứng dụng máy ảnh thú vị hơn thì mình sử dụng tính năng ImageAnalys. Nó cho phép xác định một lớp tùy chỉnh triển khai giao diện ImageAnalysis.Analyzer
và sẽ được gọi với các khung mà máy ảnh hướng đến.
Thêm bộ phân tích này vào dưới dạng một lớp bên trong trong MainActivity.kt
. Máy phân tích ghi lại độ sáng trung bình của hình ảnh. Để tạo một trình phân tích, ta ghi đè chức năng phân tích trong một lớp triển khai giao diện ImageAnalysis.Analyzer.
private class LuminosityAnaLyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Đưa bộ đệm về 0
val data = ByteArray(remaining()) //copy bộ đệm vào mảng byte
get(data)
return data
}
override fun analyze(image: ImageProxy) {
val buffer = image.planes[0].buffer
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
listener(luma)
image.close()
}
}
Với lớp triển khai giao diện ImageAnalysis.Analyzer
, tất cả những gì chúng ta cần làm là khởi tạo một phiên bản của LuminosityAnalyze
r trong ImageAnalysis
, tương tự như các trường hợp sử dụng khác và cập nhật lại hàmstartCamera()
trước khi gọi đến CameraX.bindToLifecycle()
:
private fun startCamera(){
...
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
...
cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture, imageAnalyzer)
}
Vậy là mình đã hướng dẫn các bạn làm một App CameraX đơn giản rồi, cảm ơn các bạn đã xem qua
Link source code
Tài liệu tham khảo
https://codelabs.developers.google.com/codelabs/camerax-getting-started