Welcome again to the ultimate a part of our weblog put up sequence about harnessing the ability 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 develop 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 the right way to overlay Compose UI parts on prime of your digicam preview for a richer person expertise.
- 📂 Half 4 (this put up): Utilizing adaptive APIs and the Compose animation framework to easily animate to and from tabletop mode on foldable telephones.
This put up exhibits the right way to create a UI that elegantly transitions between a full-screen and a split-screen format 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:
- Replace our dependencies to make use of the newest animation and adaptive APIs.
- Retrieve and share the hinge coordinates of our foldable machine.
- Place the digicam viewfinder above the fold when the machine 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.
Word; you possibly can observe alongside step-by-step, persevering with with the code from the third weblog put up, or take a look at the ultimate code snippet right here.
This put up takes benefit of some newer APIs, so we want to verify 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 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", 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 varieties, 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 components.
Word: Most foldables have a single show that runs throughout the hinge. In that case the hinge is just a horizontal line, so its prime and backside coordinate can be equal. Nonetheless, some foldables have two separate shows with some small house between them. That house nonetheless takes up layouting house, and you’ll 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 doubtless wish to use the identical sample in additional screens in our app, so any display screen can use this data when wanted.
To supply the hinge place for the entire composable sub-tree, we are able to 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 through the use of the currentWindowAdaptiveInfo
methodology 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 = bear in mind { CameraPreviewViewModel() }
val horizontalHinge = currentWindowAdaptiveInfo().windowPosture
.allHorizontalHingeBounds.firstOrNull()
Field(
Modifier.format { measurable, constraints ->
val placeable = measurable.measure(constraints)
format(
width = constraints.maxWidth,
peak = 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)
}
}
}
}
}
non-public enjoyable LayoutCoordinates.windowToLocal(rect: Rect): Rect =
Rect(
topLeft = windowToLocal(rect.topLeft),
bottomRight = windowToLocal(rect.bottomRight),
)
In case you bear in mind, in our final put up, we merely confirmed a full display screen digicam 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 exhibits solely on the prime half of the display screen when the machine is in tabletop mode:
As a part of this, we’ll extract all code from the earlier weblog put up 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 bear 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 out there through the placement part, 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.format { measurable, constraints ->
format(constraints.maxWidth, constraints.maxHeight) {
// Get present hinge prime, or NaN if not out there
val hingeTop = HorizontalHingeTopRuler.present(defaultValue = Float.NaN)
// Constrain the peak of the composable to the hinge prime (if out there)
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 will enhance this code by including a clean transition between the tabletop and flat modes of the machine. 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 have now 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 out there to be used inside a LookaheadScope
, you possibly can mechanically animate the bounds of a composable. Word 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 by way of the UI hierarchy utilizing a composition native (as per documentation):
val LocalLookaheadScope = compositionLocalOf { null }class MainActivity : ComponentActivity() {
override enjoyable onCreate(savedInstanceState: Bundle?) {
..
setContent {
MyApplicationTheme {
..
LookaheadScope {
CompositionLocalProvider(LocalLookaheadScope supplies this) {
Field(
Modifier.format { 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 have now some further house in our format, we are able to add a management panel that exhibits or hides based mostly on the tabletop mode. Right here, we are able to use the AnimatedVisibility
API to indicate or conceal 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 out there through the placement part, so this modifier
// *measures* with max constraints, after which *locations* the content material beneath the hinge.
non-public enjoyable Modifier.alignBelowHinge(): Modifier = this then
Modifier.format { measurable, constraints ->
format(constraints.maxWidth, constraints.maxHeight) {
// Get present hinge backside, or default to 0 if not out there
val hingeBottom = HorizontalHingeBottomRuler.present(defaultValue = 0f).roundToInt()
// Constrain the peak of the composable to the hinge backside (if out there)
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 an exquisite animated adaptive expertise!
Word: Usually, offering performance solely when in tabletop mode can be thought of a nasty observe, so that you’d have to verify the management panel is out there in non-tabletop mode as effectively. I’ll depart that so that you can implement 🙂
By combining the adaptive and animation APIs, we are able to construct highly effective adaptive functions that add foldable help with pleasant transitions.
The precept demonstrated on this weblog put up can in fact be generalized right into a separate composable element, 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 ability 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 delightful on foldable units.
We hope you loved this weblog sequence, and that it evokes you to create superb digicam experiences with Jetpack Compose. Keep in mind to test the docs for the newest updates!
Pleased coding, and thanks for becoming a member of us!