Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save JolandaVerhoef/74d4696b804736c698450bd34b5c9ff8 to your computer and use it in GitHub Desktop.
Save JolandaVerhoef/74d4696b804736c698450bd34b5c9ff8 to your computer and use it in GitHub Desktop.
Code snippets for Medium blog post "Unlocking the Power of CameraX in Jetpack Compose"

Code snippets used in the blog post series "Unlocking the Power of CameraX in Jetpack Compose". I will add snippets as blog posts are released!

Each file can be copied as-is to the MainActivity.kt of a new project. However, it doesn't include any library dependencies or adaptations to AndroidManifest.xml, so make sure to follow the steps in the blog posts themselves to get a working sample.

// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
package com.example.myapplication
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.myapplication.ui.theme.MyApplicationTheme
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
val viewModel = remember { CameraPreviewViewModel() }
CameraPreviewScreen(viewModel)
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreviewScreen(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier
) {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) {
CameraPreviewContent(viewModel, modifier)
} else {
Column(
modifier = modifier.fillMaxSize().wrapContentSize().widthIn(max = 480.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"Whoops! Looks like we need your camera to work our magic!" +
"Don't worry, we just wanna see your pretty face (and maybe some cats). " +
"Grant us permission and let's get this party started!"
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Hi there! We need your camera to work our magic! ✨\n" +
"Grant us permission and let's get this party started! \uD83C\uDF89"
}
Text(textToShow, textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Unleash the Camera!")
}
}
}
}
@Composable
fun CameraPreviewContent(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(lifecycleOwner) {
viewModel.bindToCamera(context.applicationContext, lifecycleOwner)
}
surfaceRequest?.let { request ->
CameraXViewfinder(
surfaceRequest = request,
modifier = modifier
)
}
}
class CameraPreviewViewModel : ViewModel() {
// used to set up a link between the Camera and your UI.
private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest
private val cameraPreviewUseCase = Preview.Builder().build().apply {
setSurfaceProvider { newSurfaceRequest ->
_surfaceRequest.update { newSurfaceRequest }
}
}
suspend fun bindToCamera(appContext: Context, lifecycleOwner: LifecycleOwner) {
val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext)
processCameraProvider.bindToLifecycle(
lifecycleOwner, DEFAULT_BACK_CAMERA, cameraPreviewUseCase
)
// Cancellation signals we're done with the camera
try { awaitCancellation() } finally { processCameraProvider.unbindAll() }
}
}
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
package com.example.myapplication
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.takeOrElse
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.myapplication.ui.theme.MyApplicationTheme
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import java.util.UUID
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
val viewModel = remember { CameraPreviewViewModel() }
CameraPreviewScreen(viewModel)
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreviewScreen(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier
) {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) {
CameraPreviewContent(viewModel, modifier)
} else {
Column(
modifier = modifier.fillMaxSize().wrapContentSize().widthIn(max = 480.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"Whoops! Looks like we need your camera to work our magic!" +
"Don't worry, we just wanna see your pretty face (and maybe some cats). " +
"Grant us permission and let's get this party started!"
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Hi there! We need your camera to work our magic! ✨\n" +
"Grant us permission and let's get this party started! \uD83C\uDF89"
}
Text(textToShow, textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Unleash the Camera!")
}
}
}
}
@Composable
fun CameraPreviewContent(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(lifecycleOwner) {
viewModel.bindToCamera(context.applicationContext, lifecycleOwner)
}
var autofocusRequest by remember { mutableStateOf(UUID.randomUUID() to Offset.Unspecified) }
val autofocusRequestId = autofocusRequest.first
// Show the autofocus indicator if the offset is specified
val showAutofocusIndicator = autofocusRequest.second.isSpecified
// Cache the initial coords for each autofocus request
val autofocusCoords = remember(autofocusRequestId) { autofocusRequest.second }
// Queue hiding the request for each unique autofocus tap
if (showAutofocusIndicator) {
LaunchedEffect(autofocusRequestId) {
delay(1000)
// Clear the offset to finish the request and hide the indicator
autofocusRequest = autofocusRequestId to Offset.Unspecified
}
}
surfaceRequest?.let { request ->
val coordinateTransformer = remember { MutableCoordinateTransformer() }
CameraXViewfinder(
surfaceRequest = request,
coordinateTransformer = coordinateTransformer,
modifier = modifier.pointerInput(viewModel, coordinateTransformer) {
detectTapGestures { tapCoords ->
with(coordinateTransformer) {
viewModel.tapToFocus(tapCoords.transform())
}
autofocusRequest = UUID.randomUUID() to tapCoords
}
}
)
AnimatedVisibility(
visible = showAutofocusIndicator,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.offset { autofocusCoords.takeOrElse { Offset.Zero } .round() }
.offset((-24).dp, (-24).dp)
) {
Spacer(Modifier.border(2.dp, Color.White, CircleShape).size(48.dp))
}
}
}
class CameraPreviewViewModel : ViewModel() {
// used to set up a link between the Camera and your UI.
private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest
private var surfaceMeteringPointFactory: SurfaceOrientedMeteringPointFactory? = null
private var cameraControl: CameraControl? = null
private val cameraPreviewUseCase = Preview.Builder().build().apply {
setSurfaceProvider { newSurfaceRequest ->
_surfaceRequest.update { newSurfaceRequest }
surfaceMeteringPointFactory = SurfaceOrientedMeteringPointFactory(
newSurfaceRequest.resolution.width.toFloat(),
newSurfaceRequest.resolution.height.toFloat()
)
}
}
suspend fun bindToCamera(appContext: Context, lifecycleOwner: LifecycleOwner) {
val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext)
val camera = processCameraProvider.bindToLifecycle(
lifecycleOwner, DEFAULT_BACK_CAMERA, cameraPreviewUseCase
)
cameraControl = camera.cameraControl
// Cancellation signals we're done with the camera
try { awaitCancellation() } finally {
processCameraProvider.unbindAll()
cameraControl = null
}
}
fun tapToFocus(tapCoords: Offset) {
val point = surfaceMeteringPointFactory?.createPoint(tapCoords.x, tapCoords.y)
if (point != null) {
val meteringAction = FocusMeteringAction.Builder(point).build()
cameraControl?.startFocusAndMetering(meteringAction)
}
}
}
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
package com.example.myapplication
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.takeOrElse
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.myapplication.ui.theme.MyApplicationTheme
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import nl.dionsegijn.konfetti.compose.KonfettiView
import nl.dionsegijn.konfetti.core.Party
import nl.dionsegijn.konfetti.core.Position
import nl.dionsegijn.konfetti.core.Rotation
import nl.dionsegijn.konfetti.core.emitter.Emitter
import nl.dionsegijn.konfetti.core.models.Shape
import nl.dionsegijn.konfetti.core.models.Size
import java.util.UUID
import java.util.concurrent.TimeUnit
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
val viewModel = remember { CameraPreviewViewModel() }
CameraPreviewScreen(viewModel)
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreviewScreen(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier
) {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) {
CameraPreviewContent(viewModel, modifier)
} else {
Column(
modifier = modifier.fillMaxSize().wrapContentSize().widthIn(max = 480.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"Whoops! Looks like we need your camera to work our magic!" +
"Don't worry, we just wanna see your pretty face (and maybe some cats). " +
"Grant us permission and let's get this party started!"
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Hi there! We need your camera to work our magic! ✨\n" +
"Grant us permission and let's get this party started! \uD83C\uDF89"
}
Text(textToShow, textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Unleash the Camera!")
}
}
}
}
@Composable
fun CameraPreviewContent(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(lifecycleOwner) {
viewModel.bindToCamera(context.applicationContext, lifecycleOwner)
}
var autofocusRequest by remember { mutableStateOf(UUID.randomUUID() to Offset.Unspecified) }
val autofocusRequestId = autofocusRequest.first
// Show the autofocus indicator if the offset is specified
val showAutofocusIndicator = autofocusRequest.second.isSpecified
// Cache the initial coords for each autofocus request
val autofocusCoords = remember(autofocusRequestId) { autofocusRequest.second }
// Queue hiding the request for each unique autofocus tap
if (showAutofocusIndicator) {
LaunchedEffect(autofocusRequestId) {
delay(1000)
// Clear the offset to finish the request and hide the indicator
autofocusRequest = autofocusRequestId to Offset.Unspecified
}
}
surfaceRequest?.let { request ->
val coordinateTransformer = remember { MutableCoordinateTransformer() }
CameraXViewfinder(
surfaceRequest = request,
coordinateTransformer = coordinateTransformer,
modifier = modifier.pointerInput(viewModel, coordinateTransformer) {
detectTapGestures { tapCoords ->
with(coordinateTransformer) {
viewModel.tapToFocus(tapCoords.transform())
}
autofocusRequest = UUID.randomUUID() to tapCoords
}
}
)
AnimatedVisibility(
visible = showAutofocusIndicator,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
) {
KonfettiView(
modifier = Modifier.fillMaxSize(),
parties =
listOf(
Party(
speed = 10f,
maxSpeed = 30f,
damping = 0.9f,
spread = 360,
size = listOf(Size.SMALL, Size.LARGE, Size.LARGE),
shapes = listOf(Shape.Square, Shape.Circle),
timeToLive = 500L,
fadeOutEnabled = false,
rotation = Rotation.enabled(),
colors = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def),
emitter = Emitter(duration = 100, TimeUnit.MILLISECONDS).max(30),
position = Position.Absolute(autofocusCoords.x, autofocusCoords.y),
)
)
)
Spacer(Modifier
.requiredSize(48.dp)
.offset { autofocusCoords.takeOrElse { Offset.Zero } .round() }
.offset((-24).dp, (-24).dp)
.border(2.dp, Color.White, CircleShape)
)
}
}
}
class CameraPreviewViewModel : ViewModel() {
// used to set up a link between the Camera and your UI.
private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest
private var surfaceMeteringPointFactory: SurfaceOrientedMeteringPointFactory? = null
private var cameraControl: CameraControl? = null
private val cameraPreviewUseCase = Preview.Builder().build().apply {
setSurfaceProvider { newSurfaceRequest ->
_surfaceRequest.update { newSurfaceRequest }
surfaceMeteringPointFactory = SurfaceOrientedMeteringPointFactory(
newSurfaceRequest.resolution.width.toFloat(),
newSurfaceRequest.resolution.height.toFloat()
)
}
}
suspend fun bindToCamera(appContext: Context, lifecycleOwner: LifecycleOwner) {
val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext)
val camera = processCameraProvider.bindToLifecycle(
lifecycleOwner, DEFAULT_BACK_CAMERA, cameraPreviewUseCase
)
cameraControl = camera.cameraControl
// Cancellation signals we're done with the camera
try { awaitCancellation() } finally {
processCameraProvider.unbindAll()
cameraControl = null
}
}
fun tapToFocus(tapCoords: Offset) {
val point = surfaceMeteringPointFactory?.createPoint(tapCoords.x, tapCoords.y)
if (point != null) {
val meteringAction = FocusMeteringAction.Builder(point).build()
cameraControl?.startFocusAndMetering(meteringAction)
}
}
}
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
package com.example.myapplication
import android.content.Context
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.camera.camera2.interop.Camera2Interop
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.takeOrElse
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.setFrom
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.myapplication.ui.theme.MyApplicationTheme
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.util.UUID
import kotlin.collections.isNotEmpty
import kotlin.collections.map
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
val viewModel = remember { CameraPreviewViewModel() }
CameraPreviewScreen(viewModel)
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreviewScreen(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier
) {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) {
CameraPreviewContent(viewModel, modifier)
} else {
Column(
modifier = modifier
.fillMaxSize()
.wrapContentSize()
.widthIn(max = 480.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"Whoops! Looks like we need your camera to work our magic!" +
"Don't worry, we just wanna see your pretty face (and maybe some cats). " +
"Grant us permission and let's get this party started!"
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Hi there! We need your camera to work our magic! ✨\n" +
"Grant us permission and let's get this party started! \uD83C\uDF89"
}
Text(textToShow, textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Unleash the Camera!")
}
}
}
}
@Composable
fun CameraPreviewContent(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle()
val transformationInfo by
produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) {
try {
surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo ->
value = transformationInfo
}
awaitCancellation()
} finally {
surfaceRequest?.clearTransformationInfoListener()
}
}
val shouldSpotlightFaces by remember {
derivedStateOf { sensorFaceRects.isNotEmpty() && transformationInfo != null}
}
val context = LocalContext.current
LaunchedEffect(lifecycleOwner) {
viewModel.bindToCamera(context.applicationContext, lifecycleOwner)
}
var autofocusRequest by remember { mutableStateOf(UUID.randomUUID() to Offset.Unspecified) }
val autofocusRequestId = autofocusRequest.first
// Show the autofocus indicator if the offset is specified
val showAutofocusIndicator = autofocusRequest.second.isSpecified
// Cache the initial coords for each autofocus request
val autofocusCoords = remember(autofocusRequestId) { autofocusRequest.second }
// Queue hiding the request for each unique autofocus tap
if (showAutofocusIndicator) {
LaunchedEffect(autofocusRequestId) {
delay(1000)
// Clear the offset to finish the request and hide the indicator
autofocusRequest = autofocusRequestId to Offset.Unspecified
}
}
surfaceRequest?.let { request ->
val coordinateTransformer = remember { MutableCoordinateTransformer() }
CameraXViewfinder(
surfaceRequest = request,
coordinateTransformer = coordinateTransformer,
modifier = modifier.pointerInput(viewModel, coordinateTransformer) {
detectTapGestures { tapCoords ->
with(coordinateTransformer) {
viewModel.tapToFocus(tapCoords.transform())
}
autofocusRequest = UUID.randomUUID() to tapCoords
}
}
)
AnimatedVisibility(
visible = showAutofocusIndicator,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.offset { autofocusCoords.takeOrElse { Offset.Zero }.round() }
.offset((-24).dp, (-24).dp)
) {
Spacer(Modifier.border(2.dp, Color.White, CircleShape).size(48.dp))
}
AnimatedVisibility(shouldSpotlightFaces, enter = fadeIn(), exit = fadeOut()) {
Canvas(Modifier.fillMaxSize()) {
val uiFaceRects = sensorFaceRects.transformToUiCoords(
transformationInfo = transformationInfo,
uiToBufferCoordinateTransformer = coordinateTransformer
)
// Fill the whole space with the color
drawRect(Color(0xDDE60991))
// Then extract each face and make it transparent
uiFaceRects.forEach { faceRect ->
drawRect(
Brush.radialGradient(
0.4f to Color.Black, 1f to Color.Transparent,
center = faceRect.center,
radius = faceRect.minDimension * 2f,
),
blendMode = BlendMode.DstOut
)
}
}
}
}
}
private fun List<Rect>.transformToUiCoords(
transformationInfo: SurfaceRequest.TransformationInfo?,
uiToBufferCoordinateTransformer: MutableCoordinateTransformer
): List<Rect> = this.map { sensorRect ->
val bufferToUiTransformMatrix = Matrix().apply {
setFrom(uiToBufferCoordinateTransformer.transformMatrix)
invert()
}
val sensorToBufferTransformMatrix = Matrix().apply {
transformationInfo?.let {
setFrom(it.sensorToBufferTransform)
}
}
val bufferRect = sensorToBufferTransformMatrix.map(sensorRect)
val uiRect = bufferToUiTransformMatrix.map(bufferRect)
uiRect
}
class CameraPreviewViewModel : ViewModel() {
// used to set up a link between the Camera and your UI.
private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest
private var surfaceMeteringPointFactory: SurfaceOrientedMeteringPointFactory? = null
private var cameraControl: CameraControl? = null
private val _sensorFaceRects = MutableStateFlow(listOf<Rect>())
val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow()
private val cameraPreviewUseCase = Preview.Builder()
.apply {
Camera2Interop.Extender(this)
.setCaptureRequestOption(
CaptureRequest.STATISTICS_FACE_DETECT_MODE,
CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
)
.setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
result.get(CaptureResult.STATISTICS_FACES)
?.map { face -> face.bounds.toComposeRect() }
?.toList()
?.let { faces -> _sensorFaceRects.update { faces } }
}
})
}
.build().apply {
setSurfaceProvider { newSurfaceRequest ->
_surfaceRequest.update { newSurfaceRequest }
surfaceMeteringPointFactory = SurfaceOrientedMeteringPointFactory(
newSurfaceRequest.resolution.width.toFloat(),
newSurfaceRequest.resolution.height.toFloat()
)
}
}
suspend fun bindToCamera(appContext: Context, lifecycleOwner: LifecycleOwner) {
val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext)
val camera = processCameraProvider.bindToLifecycle(
lifecycleOwner, DEFAULT_BACK_CAMERA, cameraPreviewUseCase
)
cameraControl = camera.cameraControl
// Cancellation signals we're done with the camera
try { awaitCancellation() } finally {
processCameraProvider.unbindAll()
cameraControl = null
}
}
fun tapToFocus(tapCoords: Offset) {
val point = surfaceMeteringPointFactory?.createPoint(tapCoords.x, tapCoords.y)
if (point != null) {
val meteringAction = FocusMeteringAction.Builder(point).build()
cameraControl?.startFocusAndMetering(meteringAction)
}
}
}
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
package com.example.myapplication
import android.content.Context
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.TotalCaptureResult
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.camera.camera2.interop.Camera2Interop
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.animateBounds
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.allHorizontalHingeBounds
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.takeOrElse
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.setFrom
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.HorizontalRuler
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.myapplication.ui.theme.MyApplicationTheme
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.util.UUID
import kotlin.math.roundToInt
val HorizontalHingeTopRuler = HorizontalRuler()
val HorizontalHingeBottomRuler = HorizontalRuler()
val LocalLookaheadScope = compositionLocalOf<LookaheadScope?> { null }
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
val viewModel = remember { CameraPreviewViewModel() }
val horizontalHinge = currentWindowAdaptiveInfo().windowPosture
.allHorizontalHingeBounds.firstOrNull()
LookaheadScope {
CompositionLocalProvider(LocalLookaheadScope provides this) {
Box(
Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
rulers = {
if (horizontalHinge != null) {
val bounds = coordinates.windowToLocal(horizontalHinge)
HorizontalHingeTopRuler provides bounds.top
HorizontalHingeBottomRuler provides bounds.bottom
}
}
) { placeable.place(0, 0) }
}
) {
CameraPreviewScreen(viewModel)
}
}
}
}
}
}
}
private fun LayoutCoordinates.windowToLocal(rect: Rect): Rect =
Rect(
topLeft = windowToLocal(rect.topLeft),
bottomRight = windowToLocal(rect.bottomRight),
)
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreviewScreen(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier
) {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) {
CameraPreviewContent(viewModel, modifier)
} else {
Column(
modifier = modifier
.fillMaxSize()
.wrapContentSize()
.widthIn(max = 480.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"Whoops! Looks like we need your camera to work our magic!" +
"Don't worry, we just wanna see your pretty face (and maybe some cats). " +
"Grant us permission and let's get this party started!"
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Hi there! We need your camera to work our magic! ✨\n" +
"Grant us permission and let's get this party started! \uD83C\uDF89"
}
Text(textToShow, textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Unleash the Camera!")
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun CameraPreviewContent(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(lifecycleOwner) {
viewModel.bindToCamera(context.applicationContext, lifecycleOwner)
}
val shouldHighlightFaces by remember {
derivedStateOf { sensorFaceRects.isNotEmpty() }
}
val colors = listOf(Color(0xFF09D8E6), Color(0xFFE6C709), Color(0xFFE60991))
var pickedColorIndex by rememberSaveable { mutableIntStateOf(0) }
val onColorIndexChanged = { index: Int -> pickedColorIndex = index }
val spotlightColor by animateColorAsState(colors[pickedColorIndex])
val windowPosture = currentWindowAdaptiveInfo().windowPosture
val isTabletop: Boolean = windowPosture.isTabletop
val lookaheadScope = LocalLookaheadScope.current
?: throw IllegalStateException("No LookaheadScope found")
Box(modifier.safeDrawingPadding()) {
ViewfinderContent(
surfaceRequest,
{ sensorFaceRects },
shouldHighlightFaces,
viewModel::tapToFocus,
Modifier
.fillMaxSize()
.then(if(isTabletop) Modifier.alignAboveHinge() else Modifier)
.animateBounds(lookaheadScope)
.padding(16.dp)
.clip(RoundedCornerShape(24.dp))
.border(8.dp, spotlightColor, RoundedCornerShape(24.dp))
)
AnimatedVisibility(
isTabletop,
enter = fadeIn() + slideInVertically { it / 2 },
exit = fadeOut() + slideOutVertically { it / 2 },
modifier = Modifier.alignBelowHinge()
) {
MyControlPanel(
colors = colors,
pickedColorIndex = pickedColorIndex,
onColorPicked = onColorIndexChanged,
)
}
}
}
// Place the composable above the horizontal hinge, if a hinge is present.
// Ruler values are only available during the placement phase, so this modifier
// *measures* with max constraints, and then *places* the content above the hinge.
private fun Modifier.alignAboveHinge(): Modifier = this then
Modifier.layout { measurable, constraints ->
layout(constraints.maxWidth, constraints.maxHeight) {
// Get current hinge top, or NaN if not available
val hingeTop = HorizontalHingeTopRuler.current(defaultValue = Float.NaN)
// Constrain the height of the composable to the hinge top (if available)
val childConstraints = if (hingeTop.isNaN()) constraints else
Constraints(maxHeight = hingeTop.roundToInt()).constrain(constraints)
// Place the composable above the hinge
val placeable = measurable.measure(childConstraints)
placeable.place(0, 0)
}
}
// Place the composable below the horizontal hinge, if a hinge is present.
// Ruler values are only available during the placement phase, so this modifier
// *measures* with max constraints, and then *places* the content below the hinge.
private fun Modifier.alignBelowHinge(): Modifier = this then
Modifier.layout { measurable, constraints ->
layout(constraints.maxWidth, constraints.maxHeight) {
// Get current hinge bottom, or default to 0 if not available
val hingeBottom = HorizontalHingeBottomRuler.current(defaultValue = 0f).roundToInt()
// Constrain the height of the composable to the hinge bottom (if available)
val childConstraints = Constraints(maxHeight = constraints.maxHeight - hingeBottom)
.constrain(constraints)
// Place the composable below the hinge
val placeable = measurable.measure(childConstraints)
placeable.place(0, hingeBottom)
}
}
@Composable
fun ViewfinderContent(
surfaceRequest: SurfaceRequest?,
sensorFaceRects: () -> List<Rect>,
shouldHighlightFaces: Boolean,
onTapToFocus: (Offset) -> Unit,
modifier: Modifier = Modifier
) {
val currentOnTapToFocus by rememberUpdatedState(onTapToFocus)
val transformationInfo by
produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) {
try {
surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo ->
value = transformationInfo
}
awaitCancellation()
} finally {
surfaceRequest?.clearTransformationInfoListener()
}
}
var autofocusRequest by remember { mutableStateOf(UUID.randomUUID() to Offset.Unspecified) }
val autofocusRequestId = autofocusRequest.first
// Show the autofocus indicator if the offset is specified
val showAutofocusIndicator = autofocusRequest.second.isSpecified
// Cache the initial coords for each autofocus request
val autofocusCoords = remember(autofocusRequestId) { autofocusRequest.second }
// Queue hiding the request for each unique autofocus tap
if (showAutofocusIndicator) {
LaunchedEffect(autofocusRequestId) {
delay(1000)
// Clear the offset to finish the request and hide the indicator
autofocusRequest = autofocusRequestId to Offset.Unspecified
}
}
surfaceRequest?.let { request ->
Box(modifier) {
val coordinateTransformer = remember { MutableCoordinateTransformer() }
CameraXViewfinder(
surfaceRequest = request,
coordinateTransformer = coordinateTransformer,
modifier = Modifier.pointerInput(coordinateTransformer) {
detectTapGestures { tapCoords ->
with(coordinateTransformer) {
currentOnTapToFocus(tapCoords.transform())
}
autofocusRequest = UUID.randomUUID() to tapCoords
}
}
)
AnimatedVisibility(
visible = showAutofocusIndicator,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.offset { autofocusCoords.takeOrElse { Offset.Zero }.round() }
.offset((-24).dp, (-24).dp)
) {
Spacer(
Modifier
.border(2.dp, Color.White, CircleShape)
.size(48.dp)
)
}
AnimatedVisibility(
visible = transformationInfo != null && shouldHighlightFaces,
enter = fadeIn(), exit = fadeOut()
) {
Canvas(Modifier.fillMaxSize()) {
val uiFaceRects = sensorFaceRects().transformToUiCoords(
transformationInfo = transformationInfo,
uiToBufferCoordinateTransformer = coordinateTransformer
)
// Fill the whole space with the color
drawRect(Color(0xDDE60991))
// Then extract each face and make it transparent
uiFaceRects.forEach { faceRect ->
drawRect(
Brush.radialGradient(
0.4f to Color.Black, 1f to Color.Transparent,
center = faceRect.center,
radius = faceRect.minDimension * 2f,
),
blendMode = BlendMode.DstOut
)
}
}
}
}
}
}
@Composable
fun MyControlPanel(
modifier: Modifier = Modifier,
colors: List<Color> = listOf(Color(0xFF09D8E6), Color(0xFFE6C709), Color(0xFFE60991)),
pickedColorIndex: Int = 0,
onColorPicked: (colorIndex: Int) -> Unit = { }
) {
Row(modifier.fillMaxSize(), Arrangement.SpaceAround, Alignment.CenterVertically) {
colors.forEachIndexed { colorIndex, color ->
Spacer(
Modifier
.size(72.dp)
.clip(CircleShape)
.clickable { onColorPicked(colorIndex) }
.background(color)
.then(
if (colorIndex == pickedColorIndex)
Modifier.border(2.dp, Color.Black, CircleShape)
else Modifier
))
}
}
}
private fun List<Rect>.transformToUiCoords(
transformationInfo: SurfaceRequest.TransformationInfo?,
uiToBufferCoordinateTransformer: MutableCoordinateTransformer
): List<Rect> = this.map { sensorRect ->
val bufferToUiTransformMatrix = Matrix().apply {
setFrom(uiToBufferCoordinateTransformer.transformMatrix)
invert()
}
val sensorToBufferTransformMatrix = Matrix().apply {
transformationInfo?.let {
setFrom(it.sensorToBufferTransform)
}
}
val bufferRect = sensorToBufferTransformMatrix.map(sensorRect)
val uiRect = bufferToUiTransformMatrix.map(bufferRect)
uiRect
}
class CameraPreviewViewModel : ViewModel() {
// used to set up a link between the Camera and your UI.
private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest
private var surfaceMeteringPointFactory: SurfaceOrientedMeteringPointFactory? = null
private var cameraControl: CameraControl? = null
private val _sensorFaceRects = MutableStateFlow(listOf<Rect>())
val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow()
private val cameraPreviewUseCase = Preview.Builder()
.apply {
Camera2Interop.Extender(this)
.setCaptureRequestOption(
CaptureRequest.STATISTICS_FACE_DETECT_MODE,
CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
)
.setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
result.get(CaptureResult.STATISTICS_FACES)
?.map { face -> face.bounds.toComposeRect() }
?.toList()
?.let { faces -> _sensorFaceRects.update { faces } }
}
})
}
.build().apply {
setSurfaceProvider { newSurfaceRequest ->
_surfaceRequest.update { newSurfaceRequest }
surfaceMeteringPointFactory = SurfaceOrientedMeteringPointFactory(
newSurfaceRequest.resolution.width.toFloat(),
newSurfaceRequest.resolution.height.toFloat()
)
}
}
suspend fun bindToCamera(appContext: Context, lifecycleOwner: LifecycleOwner) {
val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext)
val camera = processCameraProvider.bindToLifecycle(
lifecycleOwner, DEFAULT_BACK_CAMERA, cameraPreviewUseCase
)
cameraControl = camera.cameraControl
// Cancellation signals we're done with the camera
try {
awaitCancellation()
} finally {
processCameraProvider.unbindAll()
cameraControl = null
}
}
fun tapToFocus(tapCoords: Offset) {
val point = surfaceMeteringPointFactory?.createPoint(tapCoords.x, tapCoords.y)
if (point != null) {
val meteringAction = FocusMeteringAction.Builder(point).build()
cameraControl?.startFocusAndMetering(meteringAction)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment