Code snippets used in my conference talk: "Say Cheese! Elevate your camera flows with Jetpack Compose"
Last active
August 1, 2024 10:30
-
-
Save JolandaVerhoef/99dafc1861226969ca2f2fd374abb3e8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright 2022 Google LLC. | |
SPDX-License-Identifier: Apache-2.0 */ | |
@Composable | |
fun TakePicture_WithCameraApp(modifier: Modifier = Modifier) { | |
// #1: Set the location where the image will be stored | |
val context = LocalContext.current | |
val uri = remember { | |
FileProvider.getUriForFile( | |
context, | |
context.applicationContext.packageName + ".provider", | |
File(context.filesDir, "image.jpg") | |
) | |
} | |
// #2: Define how to launch external component | |
var captureSuccess by remember { mutableStateOf(false) } | |
val launcher = rememberLauncherForActivityResult( | |
ActivityResultContracts.TakePicture() | |
) { captureSuccess = it } | |
// #3: Show UI | |
Box(modifier, contentAlignment = Alignment.Center) { | |
if (captureSuccess) { | |
AsyncImage(uri, contentDescription = null) | |
} else { | |
Button({ launcher.launch(uri) }) { | |
Text("Take picture") | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright 2022 Google LLC. | |
SPDX-License-Identifier: Apache-2.0 */ | |
@Composable | |
fun RecordVideo_WithCameraApp(modifier: Modifier = Modifier) { | |
// #1: Set the location where the image will be stored | |
val context = LocalContext.current | |
val uri = remember { | |
FileProvider.getUriForFile( | |
context, context.applicationContext.packageName + ".provider", | |
File(context.filesDir, "video.MP4") | |
) | |
} | |
// #2: Define how to launch external component | |
var captureSuccess by remember { mutableStateOf(false) } | |
val launcher = rememberLauncherForActivityResult( | |
ActivityResultContracts.CaptureVideo() | |
) { captureSuccess = it } | |
// #3: Show UI | |
Box(modifier, contentAlignment = Alignment.Center) { | |
if (captureSuccess) { | |
Text("Successfully saved video") | |
} else { | |
Button({ launcher.launch(uri) }) { | |
Text("Record video") | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright 2022 Google LLC. | |
SPDX-License-Identifier: Apache-2.0 */ | |
// Context: https://developers.google.com/ml-kit/vision/doc-scanner/android | |
@Composable | |
fun ExternalCodeScanner(modifier: Modifier = Modifier) { | |
// #1: Create a configured scanning client | |
val context = LocalContext.current | |
val scanner = remember { | |
val options = GmsBarcodeScannerOptions.Builder() | |
.enableAutoZoom() | |
.allowManualInput() | |
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) | |
.build() | |
GmsBarcodeScanning.getClient(context, options) | |
} | |
// #2: Define how to launch external component | |
var barcode by remember { mutableStateOf<Barcode?>(null) } | |
val launchAction: () -> Unit = { | |
scanner.startScan() | |
.addOnSuccessListener { barcode = it } | |
.addOnCanceledListener { /* Deal with cancellation */ } | |
.addOnFailureListener { /* Deal with failure */ } | |
} | |
// #3: Show UI | |
val barcodeValue = barcode?.rawValue | |
if (barcodeValue != null) { | |
Text("Scanned: $barcodeValue", modifier) | |
} else { | |
Button(onClick = launchAction, modifier) { | |
Text("Start code scan!") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright 2022 Google LLC. | |
SPDX-License-Identifier: Apache-2.0 */ | |
// Context: https://developers.google.com/ml-kit/vision/doc-scanner/android | |
@Composable | |
fun ScanDocumentScreen(modifier: Modifier = Modifier) { | |
// #1: Create a configured scanning client | |
val scanner = remember { | |
val options = GmsDocumentScannerOptions.Builder() | |
.setResultFormats(RESULT_FORMAT_JPEG, RESULT_FORMAT_PDF) | |
.setScannerMode(SCANNER_MODE_FULL) | |
.setPageLimit(5) | |
.setGalleryImportAllowed(true) | |
.build() | |
GmsDocumentScanning.getClient(options) | |
} | |
// #2: Define how to launch external component | |
var scanResult by remember { mutableStateOf<GmsDocumentScanningResult?>(null) } | |
val launcher = rememberLauncherForActivityResult( | |
StartIntentSenderForResult(), | |
onResult = { result -> | |
scanResult = GmsDocumentScanningResult | |
.fromActivityResultIntent(result.data) | |
} | |
) | |
val activity = LocalContext.current.findActivity() | |
val launchAction: () -> Unit = { | |
scanner.getStartScanIntent(activity) | |
.addOnSuccessListener { | |
launcher.launch(IntentSenderRequest.Builder(it).build()) | |
} | |
.addOnFailureListener { /* Deal with failure */ } | |
} | |
// #3: Show UI | |
val firstPageImageUri = scanResult?.pages?.first() | |
val pageCount = scanResult?.pdf?.pageCount | |
if(firstPageImageUri != null) { | |
Column(modifier) { | |
Text("Page count: $pageCount") | |
AsyncImage(firstPageImageUri, null) | |
} | |
} else { | |
Button(onClick = launchAction, modifier) { | |
Text("Start scan!") | |
} | |
} | |
} | |
private fun Context.findActivity(): ComponentActivity { | |
var context = this | |
while (context is ContextWrapper) { | |
if (context is ComponentActivity) return context | |
context = context.baseContext | |
} | |
throw IllegalStateException("Cannot start scanning document without access to Activity") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright 2022 Google LLC. | |
SPDX-License-Identifier: Apache-2.0 */ | |
@Module | |
@InstallIn(ViewModelComponent::class) | |
class CameraModule { | |
@Provides | |
fun provideController( | |
@ApplicationContext context: Context | |
) = LifecycleCameraController(context) | |
} | |
class PreviewViewModel( | |
val cameraController: LifecycleCameraController | |
) : ViewModel() { | |
private var runningCameraJob: Job? = null | |
fun startCamera() { | |
stopCamera() | |
runningCameraJob = viewModelScope.launch { | |
try { | |
cameraController.bindToLifecycle( | |
CoroutineLifecycleOwner(coroutineContext) | |
) | |
awaitCancellation() | |
} finally { cameraController.unbind() } | |
} | |
} | |
fun stopCamera() = runningCameraJob?.apply { if (isActive) { cancel() } } | |
} | |
@Composable | |
fun PreviewScreen( | |
modifier: Modifier = Modifier, | |
viewModel: PreviewViewModel = viewModel() | |
) { | |
LifecycleStartEffect(viewModel) { | |
viewModel.startCamera() | |
onStopOrDispose { | |
viewModel.stopCamera() | |
} | |
} | |
AndroidView( | |
factory = { | |
PreviewView(it).apply { | |
this.controller = viewModel.cameraController | |
} | |
}, | |
modifier | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright 2022 Google LLC. | |
SPDX-License-Identifier: Apache-2.0 */ | |
// Keep in mind this is based on a snapshot and the API surface will likely change | |
// For a more up-to-date version, check out https://github.com/google/jetpack-camera-app | |
@Composable | |
fun PreviewScreen( | |
modifier: Modifier = Modifier, | |
viewModel: PreviewViewModel = viewModel() | |
) { | |
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle() | |
LifecycleStartEffect(viewModel) { | |
viewModel.startCamera() | |
onStopOrDispose { | |
viewModel.stopCamera() | |
} | |
} | |
if (surfaceRequest != null) { | |
val viewfinderArgs by surfaceRequest.produceViewfinderArgs(implementationMode) | |
viewfinderArgs?.let { args -> | |
Viewfinder( | |
surfaceRequest = args.viewfinderSurfaceRequest, | |
implementationMode = args.implementationMode, | |
transformationInfo = args.transformationInfo, | |
modifier = modifier.fillMaxSize() | |
) | |
} | |
} | |
} | |
@Composable | |
fun SurfaceRequest?.produceViewfinderArgs( | |
implementationMode: ImplementationMode | |
): State<ViewfinderArgs?> { | |
val surfaceRequest = this | |
return produceState<ViewfinderArgs?>( | |
initialValue = null, | |
surfaceRequest | |
) { | |
if (surfaceRequest != null) { | |
val viewfinderSurfaceRequest = | |
ViewfinderSurfaceRequest | |
.Builder(surfaceRequest.resolution) | |
.build() | |
surfaceRequest.addRequestCancellationListener(Runnable::run) { | |
viewfinderSurfaceRequest.markSurfaceSafeToRelease() | |
} | |
launch(start = CoroutineStart.UNDISPATCHED) { | |
try { | |
val surface = viewfinderSurfaceRequest.getSurface() | |
surfaceRequest.provideSurface(surface, Runnable::run) { | |
viewfinderSurfaceRequest.markSurfaceSafeToRelease() | |
} | |
} finally { | |
surfaceRequest.willNotProvideSurface() | |
} | |
} | |
val transformationInfos = MutableStateFlow<SurfaceRequest.TransformationInfo?>(null) | |
surfaceRequest.setTransformationInfoListener(Runnable::run) { | |
transformationInfos.value = it | |
} | |
var snapshotImplementationMode: ImplementationMode? = null | |
snapshotFlow { implementationMode } | |
.combine(transformationInfos.filterNotNull()) { implMode, transformInfo -> | |
Pair(implMode, transformInfo) | |
}.takeWhile { (implMode, _) -> | |
val shouldAbort = | |
snapshotImplementationMode != null && implMode != snapshotImplementationMode | |
if (shouldAbort) { | |
surfaceRequest.invalidate() | |
} | |
!shouldAbort | |
}.collectLatest { (implMode, transformInfo) -> | |
snapshotImplementationMode = implMode | |
value = ViewfinderArgs( | |
viewfinderSurfaceRequest, | |
implMode, | |
TransformationInfo( | |
transformInfo.rotationDegrees, | |
transformInfo.cropRect.left, | |
transformInfo.cropRect.right, | |
transformInfo.cropRect.top, | |
transformInfo.cropRect.bottom, | |
transformInfo.isMirroring | |
) | |
) | |
} | |
} | |
} | |
} | |
data class ViewfinderArgs( | |
val viewfinderSurfaceRequest: ViewfinderSurfaceRequest, | |
val implementationMode: ImplementationMode, | |
val transformationInfo: TransformationInfo | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment