Take Pictures with CameraX in Jetpack Compose
Take a picture using the device camera can be a difficult process to figure out when using XML. It is even more confusing when attempting to do so using Jetpack Compose.
This article will cover how to use CameraX in a Jetpack Compose project and display the image using Coil.
Requesting Permission
Start by adding the following camera permissions to the AndroidManifest.xml
file.
<uses-feature android:name="android.hardware.camera.any"/>
<uses-permission android:name="android.permission.CAMERA"/>
Create a requestPermissionLauncher
property in the MainActivity.kt
file.
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.i("kilo", "Permission granted")
} else {
Log.i("kilo", "Permission denied")
}
}
The block of this function will pass a Boolean
that specifies whether the request permission was granted.
Create a function that will request the camera permission.
private fun requestCameraPermission() {
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
Log.i("kilo", "Permission previously granted")
}
ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.CAMERA
) -> Log.i("kilo", "Show camera permissions dialog")
else -> requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
The when
statement will determine which step the user should be directed through by checking the camera permission from the manifest.
Make sure to call the function.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text(text = "Like and subscribe")
}
requestCameraPermission()
}
Build and run to see the OS prompt to allow camera permissions. Depending on the clicked choice, you will see the different statements logged in the console.
Showing the Camera
Add the following dependencies in the app build.gradle
file.
// CameraX
def camerax_version = "1.0.1"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:1.0.0-alpha27"
// Icons
implementation "androidx.compose.material:material-icons-extended:$compose_version"
The CameraX dependencies are self explanatory. The Icons dependency is optional, but it will be used to create the image capture button.
In a new file named CameraView.kt
, create a function that is responsible for capture the photo.
private fun takePhoto(
filenameFormat: String,
imageCapture: ImageCapture,
outputDirectory: File,
executor: Executor,
onImageCaptured: (Uri) -> Unit,
onError: (ImageCaptureException) -> Unit
) {
val photoFile = File(
outputDirectory,
SimpleDateFormat(filenameFormat, Locale.US).format(System.currentTimeMillis()) + ".jpg"
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture.takePicture(outputOptions, executor, object: ImageCapture.OnImageSavedCallback {
override fun onError(exception: ImageCaptureException) {
Log.e("kilo", "Take photo error:", exception)
onError(exception)
}
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
onImageCaptured(savedUri)
}
})
}
The arguments passed to the takePhoto
function will be used to create an output file, which is then used to build the outputOptions
. The ImageCapture
object is then used to call takePicture
which then passes the ImageCaptureException
or image Uri
through the callback of the takePhoto
function.
Before creating the composable for the CameraView
, add the following method to Context
:
private suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation ->
ProcessCameraProvider.getInstance(this).also { cameraProvider ->
cameraProvider.addListener({
continuation.resume(cameraProvider.get())
}, ContextCompat.getMainExecutor(this))
}
}
This makes it easier to get the ProcessCameraProvider
which is an asynchronous process that needs to be suspended.
Now create the composable:
@Composable
fun CameraView(
outputDirectory: File,
executor: Executor,
onImageCaptured: (Uri) -> Unit,
onError: (ImageCaptureException) -> Unit
) {
// 1
val lensFacing = CameraSelector.LENS_FACING_BACK
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val preview = Preview.Builder().build()
val previewView = remember { PreviewView(context) }
val imageCapture: ImageCapture = remember { ImageCapture.Builder().build() }
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
// 2
LaunchedEffect(lensFacing) {
val cameraProvider = context.getCameraProvider()
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)
preview.setSurfaceProvider(previewView.surfaceProvider)
}
// 3
Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier.fillMaxSize()) {
AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
IconButton(
modifier = Modifier.padding(bottom = 20.dp),
onClick = {
Log.i("kilo", "ON CLICK")
takePhoto(
filenameFormat = "yyyy-MM-dd-HH-mm-ss-SSS",
imageCapture = imageCapture,
outputDirectory = outputDirectory,
executor = executor,
onImageCaptured = onImageCaptured,
onError = onError
)
},
content = {
Icon(
imageVector = Icons.Sharp.Lens,
contentDescription = "Take picture",
tint = Color.White,
modifier = Modifier
.size(100.dp)
.padding(1.dp)
.border(1.dp, Color.White, CircleShape)
)
}
)
}
}
- This is where all the setup is taking place; making sure to keep things like
previewView
andimageCapture
in a remembered state, allowing them to recompose as needed. Launched
allows thegetCameraProvider
function to be called and await a the result, allowing theProcessCameraProvider
to be bound to the lifecycle and thepreview
to use thepreviewView
as a surface provider.- Lastly is the UI of the camera view, making the preview take up the whole screen and a button that can be used to call
takePhoto
.
Navigate back to the MainActivity.kt
file and add the following members:
private lateinit var outputDirectory: File
private lateinit var cameraExecutor: ExecutorService
private var shouldShowCamera: MutableState<Boolean> = mutableStateOf(false)
...
private fun handleImageCapture(uri: Uri) {
Log.i("kilo", "Image captured: $uri")
shouldShowCamera.value = false
}
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()
}
The focus here is on keeping the output directory, camera executor, and should show camera boolean in memory. The functions help manage those properties.
Add the CameraView
to the setContent
method in onCreate
along with setting values for outputDirectory
and cameraExecutor
:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
if (shouldShowCamera.value) {
CameraView(
outputDirectory = outputDirectory,
executor = cameraExecutor,
onImageCaptured = ::handleImageCapture,
onError = { Log.e("kilo", "View error:", it) }
)
}
}
requestCameraPermission()
outputDirectory = getOutputDirectory()
cameraExecutor = Executors.newSingleThreadExecutor()
}
The CameraView
will only be shown if shouldShowCamera
has a value of true
. By default it is false and needs to be updated when the camera permission is granted.
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.i("kilo", "Permission granted")
shouldShowCamera.value = true // 👈🏽
...
private fun requestCameraPermission() {
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
Log.i("kilo", "Permission previously granted")
shouldShowCamera.value = true // 👈🏽
}
....
Whether the camera permission was granted on the initial OS prompt or was previously granted, the shouldShowCamera.value
is updated so the camera preview will be shown on the screen. Handling denied permission should also be handled accordingly.
Build and run to see the camera preview show on screen and log the Uri
of the photo.
Showing the Photo
Add the following line to the app build.gradle
file to make Coil a dependency of the project.
// Coil
implementation "io.coil-kt:coil-compose:1.4.0"
Back in the MainActivity.kt
file, add the following properties:
private lateinit var photoUri: Uri
private var shouldShowPhoto: MutableState<Boolean> = mutableStateOf(false)
The photoUri
will be stored in memory so it can be used to display the image. shouldShowPhoto
works like shouldShowCamera
where it will be the Boolean
responsible for displaying the photo once it is captured.
In the handleImageCapture
method, add the following lines to the bottom of the function.
photoUri = uri
shouldShowPhoto.value = true
In the setContent
, below the code that shows the CameraView
add the following snippet.
if (shouldShowPhoto.value) {
Image(
painter = rememberImagePainter(photoUri),
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
The image will now be shown when shouldShowPhoto
has been updated to true
Build and run to see your fully functioning camera app 🎉