The CameraX library from Android Jetpack simplifies camera app development by providing a consistent and easy-to-use API across different Android versions. While many tutorials focus on CameraX with Compose UI, this post will guide you through building a basic camera application using CameraX with traditional XML layouts.
What is CameraX?
CameraX is a Jetpack support library that aims to make camera app development easier. It provides a higher-level API that abstracts away device-specific quirks and allows developers to focus on core camera functionality like previewing, capturing images, and recording videos. It supports lifecycle awareness, making it ideal for use with LiveData and ViewModel.
Why Use CameraX with XML?
- Stability: Provides a consistent API across various Android devices.
- Ease of Use: Simplifies camera operations like preview, capture, and analysis.
- Lifecycle Awareness: Integrates seamlessly with Android lifecycles.
- Backward Compatibility: Supports devices running Android 5.0 (API level 21) and higher.
Step-by-Step Guide to Building a Camera App with CameraX and XML
Let’s build a simple camera app that previews the camera feed and allows users to capture images using the CameraX library and traditional XML layouts.
Step 1: Set Up a New Android Project
Create a new Android project in Android Studio with an empty activity.
Step 2: Add Dependencies
Open your build.gradle file and add the following dependencies:
dependencies {
implementation "androidx.camera:camera-camera2:1.3.0"
implementation "androidx.camera:camera-lifecycle:1.3.0"
implementation "androidx.camera:camera-view:1.3.0"
implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "com.google.android.material:material:1.11.0"
// Kotlin coroutines (optional, for asynchronous operations)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
}
Sync your project to download the necessary libraries.
Step 3: Update the AndroidManifest.xml File
Add the necessary permissions and features to your AndroidManifest.xml file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.cameraxapp">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-feature android:name="android.hardware.camera.any"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<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.CameraXApp">
<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>
Ensure you request camera permission at runtime as well, as explained later.
Step 4: Create the Layout File (activity_main.xml)
Define the layout for your main activity, including a PreviewView to display the camera preview and a button to capture images:
<?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=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/captureButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Capture"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginBottom="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Step 5: Implement the Main Activity (MainActivity.kt)
Implement the main activity with the following steps:
Initialize Variables
import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.FileOutputOptions
import java.text.SimpleDateFormat
import java.util.Locale
import androidx.camera.core.*
import androidx.camera.video.*
import com.example.cameraxapp.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
private var imageCapture: ImageCapture? = null
private var videoCapture: VideoCapture? = null
private var recording: Recording? = null
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
// Set up the listeners for take photo and video capture buttons
viewBinding.captureButton.setOnClickListener { takePhoto() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
Request Camera Permissions
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
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()
}
}
}
companion object {
private const val TAG = "CameraXApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf (
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
Start Camera Function
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// 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(viewBinding.viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { l ->
Log.d("EXAMPLE", "Average luminosity: $l")
})
}
// 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, imageAnalyzer)
} catch(exc: Exception) {
Log.e(TAG, "Binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
Take Photo Function
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time stamped name and MediaStore entry.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.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 msg = "Photo capture succeeded: ${output.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
}
)
}
Luminosity Analyzer Class
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import java.nio.ByteBuffer
class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {
interface LumaListener {
fun invoke(luma: Double)
}
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) {
val buffer = image.planes[0].buffer
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
listener.invoke(luma)
image.close()
}
}
Step 6: Run the Application
Run your application on an Android device or emulator. Grant the necessary permissions when prompted. You should see the camera preview, and pressing the capture button should save an image to the device’s media storage.
Code Explanation
- Permissions: Camera permission is requested at runtime to ensure the app has access to the camera.
- PreviewView: Used to display the real-time camera feed.
- ProcessCameraProvider: Manages the lifecycle of CameraX use cases.
- ImageCapture: Handles capturing images and saving them to the device.
Conclusion
This post has demonstrated how to build a simple camera application using CameraX with traditional XML layouts. By leveraging the CameraX library, developers can simplify camera app development while ensuring compatibility and stability across a wide range of Android devices. This approach is suitable for apps that prefer or require XML layouts while still benefiting from modern camera functionalities provided by CameraX. Continue exploring CameraX to implement more advanced features such as video recording, image analysis, and custom camera controls.