Adaptive Camera: Smooth Tabletop Mode with Animations | by Jolanda Verhoef | Android Developers | Apr, 2025

Adaptive Camera: Smooth Tabletop Mode with Animations | by Jolanda Verhoef | Android Developers | Apr, 2025

Home » News » Adaptive Camera: Smooth Tabletop Mode with Animations | by Jolanda Verhoef | Android Developers | Apr, 2025
Table of Contents

Half 4 of Unlocking the Energy of CameraX in Jetpack Compose

Welcome again to the ultimate a part of our weblog submit sequence about harnessing the ability of CameraX and Compose. Within the earlier posts, we’ve created a digital camera preview display with tap-to-focus and highlight impact. Now, we’ll take our viewfinder and increase it to the bigger display!

  • 🧱 Half 1: Constructing a fundamental digital camera preview utilizing the brand new camera-compose artifact. We lined permission dealing with and fundamental integration.
  • 👆 Half 2: Utilizing the Compose gesture system, graphics, and coroutines to implement a visible tap-to-focus.
  • 🔦 Half 3: Exploring overlay Compose UI parts on high of your digital camera preview for a richer consumer expertise.
  • 📂 Half 4 (this submit): Utilizing adaptive APIs and the Compose animation framework to easily animate to and from tabletop mode on foldable telephones.

This submit reveals create a UI that elegantly transitions between a full-screen and a split-screen structure when a foldable machine enters tabletop mode. We will use Compose’s animation APIs to easily animate this transition.

Right here’s the ultimate consequence:

Constructing upon the ideas and code from the earlier posts, we’ll accomplish this in 5 logical steps:

  1. Replace our dependencies to make use of the most recent animation and adaptive APIs.
  2. Retrieve and share the hinge coordinates of our foldable machine.
  3. Place the digital camera viewfinder above the fold when the machine is in tabletop mode.
  4. Animate the transition between full-screen and top-half solely viewfinder.
  5. Create supporting content material to show within the backside half of the display when in tabletop mode.

Observe; you may observe alongside step-by-step, persevering with with the code from the third weblog submit, or take a look at the ultimate code snippet right here.

This submit takes benefit of some newer APIs, so we want to ensure to replace our dependencies to their newest variations. We are going to 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 take care of foldable ideas corresponding to tabletop mode and bodily machine 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", title = "compose-bom-beta", model.ref = "composeBom" }
androidx-material3-adaptive = { group = "androidx.compose.material3.adaptive", title = "adaptive" }
..

#construct.gradle.kts
..
dependencies {
..
implementation(libs.androidx.material3.adaptive)
}

We’re aiming to align our UI elements primarily based on the place of the foldable’s hinge. As a result of foldables are available in many shapes and varieties, we should always align our UI to the precise place of the highest and backside of the hinge, as a substitute of merely breaking apart the display into two components.

Observe: Most foldables have a single show that runs throughout the hinge. In that case the hinge is just a horizontal line, so its high and backside coordinate can be equal. Nonetheless, some foldables have two separate shows with some small area between them. That area nonetheless takes up layouting area, and you’ll nonetheless draw issues in that non-existent area.

Principally, we need to know the highest and the underside y-coordinate of the primary horizontal hinge:

Additionally, we probably need to use the identical sample in additional screens in our app, so any display can use this info when wanted.

To offer 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 basis of our UI hierarchy, any UI element 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 offers bounds.high
HorizontalHingeBottomRuler offers bounds.backside
}
}
) { placeable.place(0, 0) }
}
) {
CameraPreviewScreen(viewModel)
}
}
}
}
}

non-public enjoyable LayoutCoordinates.windowToLocal(rect: Rect): Rect =
Rect(
topLeft = windowToLocal(rect.topLeft),
bottomRight = windowToLocal(rect.bottomRight),
)

When you keep in mind, in our final submit, we merely confirmed a full display digital camera viewfinder. That doesn’t look nice whereas our foldable machine is in tabletop mode:

So, let’s wrap our viewfinder inside a container, and be sure that that container reveals solely on the high half of the display when the machine is in tabletop mode:

As a part of this, we’ll extract all code from the earlier weblog submit 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 = Shade(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 accessible throughout the placement section, so this modifier
// *measures* with max constraints, after which *locations* the content material above the hinge.
non-public enjoyable Modifier.alignAboveHinge(): Modifier = this then
Modifier.structure { measurable, constraints ->
structure(constraints.maxWidth, constraints.maxHeight) {
// Get present hinge high, or NaN if not accessible
val hingeTop = HorizontalHingeTopRuler.present(defaultValue = Float.NaN)

// Constrain the peak of the composable to the hinge high (if accessible)
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 section. So, on this case, we make the ViewfinderContent composable measure with full constraints, however then place itself solely above the hinge.

We will enhance this code by including a easy transition between the tabletop and flat modes of the machine. Proper now, when the consumer strikes between these two states, the UI jumps from one model to the opposite, making a jarring expertise:

Fortunately, now we have 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 accessible to be used inside a LookaheadScope , you may mechanically animate the bounds of a composable. Observe that we’ll want so as to add the LookaheadScope on the high stage so the rulers take it into consideration, after which cross it on by means of 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 offers 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 now we have some further area in our structure, we will add a management panel that reveals or hides primarily based on the tabletop mode. Right here, we will use the AnimatedVisibility API to indicate or disguise the panel, together with the identical rulers as above to place the panel under the hinge:

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
enjoyable CameraPreviewContent(
..
) {
..

val colours = listOf(Shade(0xFF09D8E6), Shade(0xFFE6C709), Shade(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 under the horizontal hinge, if a hinge is current.
// Ruler values are solely accessible throughout the placement section, so this modifier
// *measures* with max constraints, after which *locations* the content material under the hinge.
non-public 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 accessible
val hingeBottom = HorizontalHingeBottomRuler.present(defaultValue = 0f).roundToInt()

// Constrain the peak of the composable to the hinge backside (if accessible)
val childConstraints = Constraints(maxHeight = constraints.maxHeight - hingeBottom)
.constrain(constraints)

// Place the composable under the hinge
val placeable = measurable.measure(childConstraints)
placeable.place(0, hingeBottom)
}
}

And with that, we’ll have a good looking animated adaptive expertise!

Observe: Typically, offering performance solely when in tabletop mode can be thought of a foul follow, so that you’d have to ensure the management panel is on the market in non-tabletop mode as effectively. I’ll depart that so that you can implement 🙂

By combining the adaptive and animation APIs, we will construct highly effective adaptive purposes that add foldable help with pleasant transitions.

The precept demonstrated on this weblog submit can in fact be generalized right into a separate composable element, so it may be reused throughout screens.

You could find the total code snippet right here. And with that, we’ve concluded our journey of exploring the ability of CameraX and Compose! We’ve constructed a fundamental digital camera preview, added tap-to-focus performance, overlaid a dynamic highlight impact, and at last, made our app adaptive and exquisite on foldable gadgets.

We hope you loved this weblog sequence, and that it evokes you to create superb digital camera experiences with Jetpack Compose. Bear in mind to verify the docs for the most recent updates!

Completely happy coding, and thanks for becoming a member of us!

Supply hyperlink

author avatar
roosho Senior Engineer (Technical Services)
I am Rakib Raihan RooSho, Jack of all IT Trades. You got it right. Good for nothing. I try a lot of things and fail more than that. That's how I learn. Whenever I succeed, I note that in my cookbook. Eventually, that became my blog. 
share this article.

Enjoying my articles?

Sign up to get new content delivered straight to your inbox.

Please enable JavaScript in your browser to complete this form.
Name