Welcome again to the ultimate a part of our weblog publish collection about harnessing the facility of CameraX and Compose. Within the earlier posts, we’ve created a digicam preview display screen with tap-to-focus and highlight impact. Now, we are going to take our viewfinder and increase it to the bigger display screen!
- 🧱 Half 1: Constructing a primary digicam preview utilizing the brand new camera-compose artifact. We coated permission dealing with and primary integration.
- 👆 Half 2: Utilizing the Compose gesture system, graphics, and coroutines to implement a visible tap-to-focus.
- 🔦 Half 3: Exploring methods to overlay Compose UI parts on prime of your digicam preview for a richer person expertise.
- 📂 Half 4 (this publish): Utilizing adaptive APIs and the Compose animation framework to easily animate to and from tabletop mode on foldable telephones.
This publish reveals methods to create a UI that elegantly transitions between a full-screen and a split-screen structure when a foldable system enters tabletop mode. We are able to use Compose’s animation APIs to easily animate this transition.
Right here’s the ultimate outcome:
Constructing upon the ideas and code from the earlier posts, we’ll accomplish this in 5 logical steps:
- Replace our dependencies to make use of the newest animation and adaptive APIs.
- Retrieve and share the hinge coordinates of our foldable system.
- Place the digicam viewfinder above the fold when the system is in tabletop mode.
- Animate the transition between full-screen and top-half solely viewfinder.
- Create supporting content material to show within the backside half of the display screen when in tabletop mode.
Be aware; you’ll be able to observe alongside step-by-step, persevering with with the code from the third weblog publish, or try the ultimate code snippet right here.
This publish takes benefit of some newer APIs, so we want to ensure to replace our dependencies to their newest variations. We’ll use the model new animateBounds
API, launched in Compose 1.8.
We’ll additionally add a dependency on material3-adaptive
, the artifact that helps us cope with foldable ideas corresponding to tabletop mode and bodily system hinges:
#libs.variations.toml[versions]
kotlin = "2.1.20"
composeBom = "2025.03.01"
camerax = "1.5.0-alpha06"
accompanist = "0.37.2"
..
[libraries]
androidx-compose-bom = { group = "androidx.compose", identify = "compose-bom-beta", model.ref = "composeBom" }
androidx-material3-adaptive = { group = "androidx.compose.material3.adaptive", identify = "adaptive" }
..
#construct.gradle.kts
..
dependencies {
..
implementation(libs.androidx.material3.adaptive)
}
We’re aiming to align our UI elements based mostly on the place of the foldable’s hinge. As a result of foldables are available in many shapes and types, we should always align our UI to the precise place of the highest and backside of the hinge, as an alternative of merely breaking apart the display screen into two elements.
Be aware: Most foldables have a single show that runs throughout the hinge. In that case the hinge is solely a horizontal line, so its prime and backside coordinate can be equal. Nevertheless, some foldables have two separate shows with some small house between them. That house nonetheless takes up layouting house, and you may nonetheless draw issues in that non-existent house.
Principally, we wish to know the highest and the underside y-coordinate of the primary horizontal hinge:
Additionally, we seemingly wish to use the identical sample in additional screens in our app, so any display screen can use this info when wanted.
To supply the hinge place for the entire composable sub-tree, we will use rulers, a brand new UI idea launched in Compose 1.7.0. By defining a horizontal ruler and offering it from the foundation of our UI hierarchy, any UI part can then use that ruler to align itself or its kids to.
Since hinges can have a non-zero thickness, let’s present two horizontal rulers, one for the highest and one for the underside y-coordinate of the primary horizontal hinge. We’ll retrieve the precise coordinates by utilizing the currentWindowAdaptiveInfo
technique in material3-adaptive
.
val HorizontalHingeTopRuler = HorizontalRuler()
val HorizontalHingeBottomRuler = HorizontalRuler()class MainActivity : ComponentActivity() {
override enjoyable onCreate(savedInstanceState: Bundle?) {
tremendous.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
val viewModel = keep in mind { CameraPreviewViewModel() }
val horizontalHinge = currentWindowAdaptiveInfo().windowPosture
.allHorizontalHingeBounds.firstOrNull()
Field(
Modifier.structure { measurable, constraints ->
val placeable = measurable.measure(constraints)
structure(
width = constraints.maxWidth,
top = constraints.maxHeight,
rulers = {
if (horizontalHinge != null) {
val bounds = coordinates.windowToLocal(horizontalHinge)
HorizontalHingeTopRuler supplies bounds.prime
HorizontalHingeBottomRuler supplies bounds.backside
}
}
) { placeable.place(0, 0) }
}
) {
CameraPreviewScreen(viewModel)
}
}
}
}
}
personal enjoyable LayoutCoordinates.windowToLocal(rect: Rect): Rect =
Rect(
topLeft = windowToLocal(rect.topLeft),
bottomRight = windowToLocal(rect.bottomRight),
)
For those who keep in mind, in our final publish, we merely confirmed a full display screen digicam viewfinder. That doesn’t look nice whereas our foldable system is in tabletop mode:
So, let’s wrap our viewfinder inside a container, and guarantee that that container reveals solely on the prime half of the display screen when the system is in tabletop mode:
As a part of this, we’ll extract all code from the earlier weblog publish into its personal ViewfinderContent
composable.
@Composable
enjoyable CameraPreviewContent(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.present
) {
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle()
val context = LocalContext.present
LaunchedEffect(lifecycleOwner) {
viewModel.bindToCamera(context.applicationContext, lifecycleOwner)
}
val shouldHighlightFaces by keep in mind {
derivedStateOf { sensorFaceRects.isNotEmpty() }
}val spotlightColor = Coloration(0xFFE60991)
val windowPosture = currentWindowAdaptiveInfo().windowPosture
val isTabletop: Boolean = windowPosture.isTabletop
Field(modifier.safeDrawingPadding()) {
ViewfinderContent(
surfaceRequest,
{ sensorFaceRects },
shouldHighlightFaces: Boolean,
viewModel::tapToFocus,
Modifier
.fillMaxSize()
.then(if (isTabletop) Modifier.alignAboveHinge() else Modifier)
.padding(16.dp)
.clip(RoundedCornerShape(24.dp))
.border(8.dp, spotlightColor, RoundedCornerShape(24.dp))
)
}
}
// Place the composable above the horizontal hinge, if a hinge is current.
// Ruler values are solely obtainable throughout the placement part, so this modifier
// *measures* with max constraints, after which *locations* the content material above the hinge.
personal enjoyable Modifier.alignAboveHinge(): Modifier = this then
Modifier.structure { measurable, constraints ->
structure(constraints.maxWidth, constraints.maxHeight) {
// Get present hinge prime, or NaN if not obtainable
val hingeTop = HorizontalHingeTopRuler.present(defaultValue = Float.NaN)
// Constrain the peak of the composable to the hinge prime (if obtainable)
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)
}
}
One factor to notice is that we’ll solely have entry to the rulers as soon as within the placement part. So, on this case, we make the ViewfinderContent
composable measure with full constraints, however then place itself solely above the hinge.
We are able to enhance this code by including a clean transition between the tabletop and flat modes of the system. Proper now, when the person strikes between these two states, the UI jumps from one model to the opposite, making a jarring expertise:
Fortunately, we’ve the brand new animation APIs so as to add computerized transitions between the 2 states. With Modifier.animateBounds()
(new in Compose 1.8), that’s obtainable to be used inside a LookaheadScope
, you’ll be able to routinely animate the bounds of a composable. Be aware that we’ll want so as to add the LookaheadScope
on the prime degree so the rulers take it under consideration, after which cross it on via the UI hierarchy utilizing a composition native (as per documentation):
val LocalLookaheadScope = compositionLocalOf<LookaheadScope?> { null }class MainActivity : ComponentActivity() {
override enjoyable onCreate(savedInstanceState: Bundle?) {
..
setContent {
MyApplicationTheme {
..
LookaheadScope {
CompositionLocalProvider(LocalLookaheadScope supplies this) {
Field(
Modifier.structure { measurable, constraints ->
..
}
) {
CameraPreviewScreen(viewModel)
}
}
}
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
enjoyable CameraPreviewContent(
viewModel: CameraPreviewViewModel,
modifier: Modifier = Modifier,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.present
) {
..
val isTabletop: Boolean = windowPosture.isTabletop
val lookaheadScope = LocalLookaheadScope.present
?: throw IllegalStateException("No LookaheadScope discovered")
Field(modifier.safeDrawingPadding()) {
ViewfinderContent(
..
modifier = Modifier
.fillMaxSize()
.then(if (isTabletop) Modifier.alignAboveHinge() else Modifier)
.animateBounds(this@LookaheadScope)
.padding(16.dp)
.clip(RoundedCornerShape(24.dp))
.border(8.dp, spotlightColor, RoundedCornerShape(24.dp))
)
}
}
Now that we’ve some additional house in our structure, we will add a management panel that reveals or hides based mostly on the tabletop mode. Right here, we will use the AnimatedVisibility
API to indicate or cover the panel, together with the identical rulers as above to place the panel beneath the hinge:
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
enjoyable CameraPreviewContent(
..
) {
..val colours = listOf(Coloration(0xFF09D8E6), Coloration(0xFFE6C709), Coloration(0xFFE60991))
var pickedColorIndex by rememberSaveable { mutableIntStateOf(0) }
val onColorIndexChanged = { index: Int -> pickedColorIndex = index }
val spotlightColor by animateColorAsState(colours[pickedColorIndex])
Field(modifier.safeDrawingPadding()) {
ViewfinderContent(
..
)
AnimatedVisibility(
isTabletop,
enter = fadeIn() + slideInVertically { it / 2 },
exit = fadeOut() + slideOutVertically { it / 2 },
modifier = Modifier.alignBelowHinge()
) {
MyControlPanel(
colours = colours,
pickedColorIndex = pickedColorIndex,
onColorPicked = onColorIndexChanged,
)
}
}
}
// Place the composable beneath the horizontal hinge, if a hinge is current.
// Ruler values are solely obtainable throughout the placement part, so this modifier
// *measures* with max constraints, after which *locations* the content material beneath the hinge.
personal enjoyable Modifier.alignBelowHinge(): Modifier = this then
Modifier.structure { measurable, constraints ->
structure(constraints.maxWidth, constraints.maxHeight) {
// Get present hinge backside, or default to 0 if not obtainable
val hingeBottom = HorizontalHingeBottomRuler.present(defaultValue = 0f).roundToInt()
// Constrain the peak of the composable to the hinge backside (if obtainable)
val childConstraints = Constraints(maxHeight = constraints.maxHeight - hingeBottom)
.constrain(constraints)
// Place the composable beneath the hinge
val placeable = measurable.measure(childConstraints)
placeable.place(0, hingeBottom)
}
}
And with that, we’ll have a lovely animated adaptive expertise!
Be aware: Typically, offering performance solely when in tabletop mode can be thought of a nasty observe, so that you’d have to ensure the management panel is out there in non-tabletop mode as nicely. I’ll go away that so that you can implement 🙂
By combining the adaptive and animation APIs, we will construct highly effective adaptive purposes that add foldable assist with pleasant transitions.
The precept demonstrated on this weblog publish can after all be generalized right into a separate composable part, so it may be reused throughout screens.
You will discover the complete code snippet right here. And with that, we’ve concluded our journey of exploring the facility of CameraX and Compose! We’ve constructed a primary digicam preview, added tap-to-focus performance, overlaid a dynamic highlight impact, and at last, made our app adaptive and exquisite on foldable units.
We hope you loved this weblog collection, and that it conjures up you to create wonderful digicam experiences with Jetpack Compose. Keep in mind to examine the docs for the newest updates!
Comfortable coding, and thanks for becoming a member of us!