
Parallax Effect with SensorManager using Jetpack Compose
Recently, an implementation of stunning Parallax effect in SwiftUI by Philip Davis has been making rounds in the Android Community too.
So, I decided to try implementing the same using Jetpack Compose instead. The results turned out pretty decent and also well received by our lovely Android Twitter Community. 😁
Implementation
The following explanation is completely based on this gist.
Fetching Sensor data
The @Composable
views will be repositioned according to the orientation of the device. And we’ll be needing the orientation values along the ‘Pitch’ and the ‘Roll’ axes.

I’ve referred to this StackOverflow reply for my implementation of fetching the Pitch and Roll values.
Register SensorManager listeners
Using DisposableEffect
we’ll be initialising the SensorManager
and also register listeners for the GRAVITY
& MAGNETIC_FIELD
sensors.
Finally the Pitch and Roll are received within the @Composable
using Channel.receiveAsFlow().collect()
.
DisposableEffect
One may say that we could’ve LaunchedEffect
instead since we’re only collecting our sensor’s data Flow. I’ll explain why I went with a DisposableEffect
in the end (it has something to do with onDispose{}
).
Now, once we have the Pitch and Roll values from our sensor, we’ll now be looking into how we can reposition various @Composable
views to achieve the parallax effect. Philip Davis has himself graciously hinted at his implementation:
- repositioning the Image Card
- repositioning the Glow Shadow (reverse of the Card). I used Philip’s Glow Shadow implementation as reference for this.
- showing a subtle Card edge when tilted, for the pronounced 3D effect
- adding a parallax to Image inside (reverse of the Card)

Image Card
[1] we’re using Modifier.offset
to reposition our @Composable
views.
[2] Roll is along the y-axis while Pitch is along the x-axis. So, the x-offset will be decided by the roll
while y-offset by the pitch
.
[3] We bring about the inside Image’s parallax effect by adjusting the horizontalBias
of @Composable Image()
alignment parameter.

Glow Shadow
[1] Glow Shadow’s offset will reposition quicker than the Image Card and in the opposite direction.
[2] It’s size will be smaller than the Image Card.
- When the device is kept on a Flat surface (all orientation values close to 0), the glow shadow should not be visible.
- The darker portion of shadows are generally smaller than the actual object.
[3] Instead of a traditional shadow, use Modifier.blur()
to make the Image Card appear translucent which is letting in light onto the surface.

Card Edge
[1] Card Edge’s offset repositioning will be slightly flower than Image Card.
- The edge shouldn’t be visible when kept on a Flat surface.
- The edge should ‘just’ be visible when the device is tilted to give the impression of a thin 3D card.
[2] It’s size will be same of the Image Card.

Points of Caution
Unregister cancel fetching of sensor data in DisposableEffect.onDispose{..}
We must prevent unnecessary leaking of SensorManager
when this view is removed from the composition. That is the reason why I used DisposableEffect
instead of LaunchedEffect
since we get this convenient onDispose{}
callback where we can clean up any unnecessary data upon leaving composition.
Use the right Modifier.offset
method
Jetpack Compose provides two kinds of Offset modifiers:
.offset(x: Dp = 0.dp, y: Dp = 0.dp)
.offset(offset: Density.() -> IntOffset)
While these do the same thing i.e., set positional offset to the @Composable
view, IF we are going to use dynamic offsets (for example, offsets changed due to certain animation or user action etc.) instead of fixed offsets, we should always use .offset(offset: Density.() -> IntOffset
i.e., the lambda version. The reason being, the latter method avoids recomposition when the offset is changing values, thus significantly improving the performance. Even the documentation says,
This modifier is designed to be used for offsets that change, possibly due to user interactions. It avoids recomposition when the offset is changing, and also adds a graphics layer that prevents unnecessary redrawing of the context when the offset is changing.
Concluding
While this isn’t the most performance efficient implementation, hope this can help as a handy reference for the same.
Jetpack Compose and Swift UI are the best tools to realise high quality visual effects in such a short period time. I certainly believe that these two platforms will empower Mobile Developers to create beautiful experiences for the users. 🥳
Do share your solutions, suggestions and feedback on Twitter. Can’t wait to see what the community comes up with next 😁 It’s a nice time to be an Mobile Developer.