Giới thiệu
API Camera cũ (android.hardware.Camera) và MediaStore.ACTION_IMAGE_CAPTURE intent đã **deprecated**. Google khuyến nghị dùng **CameraX** (Jetpack) - API hiện đại, lifecycle-aware, xử lý tự động rotation, aspect ratio, và tương thích ngược.
Yêu cầu
minSdk 21+(Android 5.0+)- Dependencies CameraX (xem bên dưới)
Các bước thực hiện
Bước 1: Tạo dự án mới
Android Studio → File → New Project → Empty View Activity.
Bước 2: Thêm dependencies (build.gradle.kts)
dependencies {
// CameraX core
def camerax_version = "1.4.0-alpha02"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
// ViewBinding
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
}
Bước 3: Cấu hình AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="com.example.cameraxdemo">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- Android 10+ không cần WRITE_EXTERNAL_STORAGE cho MediaStore -->
<uses-feature android:name="android.hardware.camera.any" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CameraXDemo">
<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>
Bước 4: Thiết kế giao diện (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/btnCapture"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btnCapture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="Chụp ảnh"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Bước 5: Code chính (MainActivity.kt)
package com.example.cameraxdemo
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
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 androidx.lifecycle.LifecycleOwner
import com.example.cameraxdemo.databinding.ActivityMainBinding
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var imageCapture: ImageCapture? = null
private val cameraExecutor = Executors.newSingleThreadExecutor()
private val cameraProviderFuture: ListenableFuture
by lazy { ProcessCameraProvider.getInstance(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
binding.btnCapture.setOnClickListener { takePhoto() }
}
private fun startCamera() {
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
bindCameraUseCases(cameraProvider)
}, ContextCompat.getMainExecutor(this))
}
private fun bindCameraUseCases(cameraProvider: ProcessCameraProvider) {
// Preview
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
// ImageCapture
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
// CameraSelector (back camera mặc định)
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this as LifecycleOwner, cameraSelector, preview, imageCapture
)
} catch (e: Exception) {
Log.e("CameraX", "Use case binding failed", e)
}
}
private fun takePhoto() {
val imageCapture = this.imageCapture ?: return
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val contentValues = android.content.ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "IMG_$timeStamp.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/CameraXDemo")
}
}
val outputOptions = ImageCapture.OutputFileOptions.Builder(
contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
).build()
imageCapture.takePicture(
outputOptions,
cameraExecutor,
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
runOnUiThread {
Toast.makeText(this@MainActivity, "Lỗi chụp ảnh: ${exc.message}", Toast.LENGTH_SHORT).show()
Log.e("CameraX", "Photo capture failed", exc)
}
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
runOnUiThread {
Toast.makeText(this@MainActivity, "Đã lưu ảnh vào thư viện", Toast.LENGTH_SHORT).show()
}
}
}
)
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this, "Cần quyền Camera để chụp ảnh", Toast.LENGTH_SHORT).show()
finish()
}
}
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
companion object {
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
}
So sánh: CameraX vs Intent ACTION_IMAGE_CAPTURE vs Camera2
| Đặc điểm | CameraX (Khuyến nghị) | Intent ACTION_IMAGE_CAPTURE | Camera2 API |
|---|---|---|---|
| Độ phức tạp | Trung bình | Thấp | Rất cao |
| Tùy biến UI | Hoàn toàn (PreviewView) | Không (mở app camera hệ thống) | Hoàn toàn |
| Xử lý rotation | Tự động | Tự động | Manual |
| Lifecycle aware | Có | Không | Manual |
| Extensions (HDR, Night, Bokeh) | Có | Không | Manual |
| Kết quả trả về | File/Byte array/Uri | Thumbnail bitmap (nhỏ) | ImageReader/Buffer |
Lưu ý quan trọng
- CameraX cần
androidx.camera:camera-viewchoPreviewView. - Luôn bind lifecycle:
cameraProvider.bindToLifecycle(this, ...). - Lưu ảnh dùng MediaStore (Android 10+) thay vì ghi file trực tiếp.
- Request permission
CAMERAtrước khi start camera. - Shutdown
cameraExecutortrongonDestroy(). - Test trên thiết bị thật (emulator camera hạn chế).