Blog Infos

This image is generated by Midjourney


As part of my daily routine, I often explore the latest developments on platforms like X and Medium. One morning, while scrolling through X, I stumbled upon a GitHub repository shared by KavSoft that immediately caught my attention. This project, hosted at KavSoft-Tutorials-iOS, showcased a mesmerizing parallax carousel. Its visual appeal and user experience were simply captivating.

Parallax Carousel Slider Using TabView in SwiftUI 2.0 by kenfai

In that moment, I knew I had to roll up my sleeves and recreate this magic in Jetpack Compose for Android. This article serves as a bridge between these two worlds, illustrating how we can transform the SwiftUI Parallax Carousel into its Jetpack Compose equivalent.

Sample project

Our journey begins with the examination of the core components, and for those interested in exploring the detailed code snippets and the complete source code, you can find them all here: ParallaxCarouselCompose.

Let’s commence this transformation by delving into the key components.

TabView → HorizontalPager

To bridge the gap between SwiftUI’s TabView and Jetpack Compose, we turn our attention to HorizontalPager. SwiftUI’s TabView offers a delightful way to swipe through a collection of views seamlessly. In the realm of Android, our transition starts with HorizontalPager.

While it’s important to note that HorizontalPager is still considered experimental, it proves to be a fitting choice for replicating the core functionality of TabView in SwiftUI. With HorizontalPager, we’re able to navigate through a series of images with grace and fluidity, maintaining the desired user experience.

// Padding values
private val cardPadding = 25.dp
private val imagePadding = 10.dp
// Shadow and shape values for the card
private val shadowElevation = 15.dp
private val borderRadius = 15.dp
private val shape = RoundedCornerShape(borderRadius)
// Offset for the parallax effect
private val xOffset = cardPadding.value * 2
fun ParallaxCarousel() {
// Get screen dimensions and density
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val density = LocalDensity.current.density
// List of image resources
val images = listOf(
// Create a pager state
val pagerState = rememberPagerState {
// Calculate the height for the pager
val pagerHeight = screenHeight / 1.5f
// HorizontalPager composable: Swiping through images
state = pagerState,
modifier = Modifier
) { page ->
// Calculate the parallax offset for the current page
val parallaxOffset = pagerState.getOffsetFractionForPage(page) * screenWidth.value
// Call ParallaxCarouselItem with image resource and parameters



No results found.

The parallaxOffset calculation is used to determine the position offset of the current page in the carousel. Here’s why:

  • pagerState.getOffsetFractionForPage(page): This function retrieves a fractional value representing how far the current page is from its snapped position. It varies between -0.5 (if the page is offset towards the start of the layout) to 0.5 (if the page is offset towards the end of the layout). When the current page is in the snapped position, this value is 0.0. It’s a useful indicator of the page’s position within the carousel.
  • screenWidth: This is the width of the screen or the display area. Multiplying the fractional offset by the screen width helps scale the offset value to match the screen’s dimensions. This step ensures that the parallax effect moves the image proportionally across the screen, making the effect visually pleasing and responsive to the screen size.
GeometryReader → Canvas

Upon examining the SwiftUI code, I noticed the use of GeometryReader, a critical component in achieving the parallax effect. In the realm of Jetpack Compose, we enlist the assistance of Canvas to bring this effect to life.

However, there’s a subtle difference. Unlike SwiftUI, where you can place an image inside GeometryReader with .aspectRatio(contentMode: .fill) to achieve the correct image ratio, Jetpack Compose takes a slightly different approach. Within Canvas, we can’t directly use Compose Image. Instead, we employ the drawBitmap function of Canvas to render the images.

To replicate the behavior observed on iOS — an image that spans the full width (equivalent to the screen size) while maintaining the correct height — we delve into some calculation. This involves ensuring that our drawn image maintains the correct aspect ratio.

For those curious, here’s a glimpse of the calculateDrawSize function that handles this calculation.

// Function to calculate draw size for the image
private fun ImageBitmap.calculateDrawSize(density: Float, screenWidth: Dp, pagerHeight: Dp): IntSize {
val originalImageWidth = width / density
val originalImageHeight = height / density
val frameAspectRatio = screenWidth / pagerHeight
val imageAspectRatio = originalImageWidth / originalImageHeight
val drawWidth = xOffset + if (frameAspectRatio > imageAspectRatio) {
} else {
pagerHeight.value * imageAspectRatio
val drawHeight = if (frameAspectRatio > imageAspectRatio) {
screenWidth.value / imageAspectRatio
} else {
return IntSize(drawWidth.toIntPx(density), drawHeight.toIntPx(density))
// Extension function to convert Float to Int in pixels
private fun Float.toIntPx(density: Float) = (this * density).roundToInt()

Now, let’s demystify a crucial aspect: the usage of Canvas translateparallaxOffset and xOffset. These elements are pivotal in creating the parallax effect we’re striving for in Jetpack Compose.

fun ParallaxCarouselItem(
imageResId: Int,
parallaxOffset: Float,
pagerHeight: Dp,
screenWidth: Dp,
density: Float,
) {
// Load the image bitmap
val imageBitmap = ImageBitmap.imageResource(id = imageResId)
// Calculate the draw size for the image
val drawSize = imageBitmap.calculateDrawSize(density, screenWidth, pagerHeight)
// Card composable for the item
modifier = Modifier
.background(Color.White, shape)
.shadow(elevation = shadowElevation, shape = shape)
) {
// Canvas for drawing the image with parallax effect
modifier = Modifier
) {
// Translate the canvas for parallax effect
translate(left = parallaxOffset * density) {
// Draw the image
image = imageBitmap,
srcSize = IntSize(imageBitmap.width, imageBitmap.height),
dstOffset = IntOffset(-xOffset.toIntPx(density), 0),
dstSize = drawSize,

xOffset is defined as an offset value for the parallax effect. It’s calculated as twice the value of cardPadding. This offset determines how much the image is shifted horizontally within the Canvas.

The translate function within the Canvas is used to shift the Canvas horizontally by an amount specified by parallaxOffset. This horizontal translation creates the parallax effect, making the image appear to move horizontally as the user interacts with the carousel.

Result: ParallaxCarousel with Jetpack Compose


With that, we’ve successfully bridged the gap between SwiftUI and Jetpack Compose, transforming a captivating parallax carousel from one platform to another. As we’ve discovered, while the tools and methods may differ, the end result is equally stunning. Jetpack Compose empowers us to bring our creative visions to life on the Android platform, and this journey is a testament to its flexibility and capabilities.

Before we conclude, it’s worth mentioning that the brilliant Chris Banes has also explored parallax effects in Jetpack Compose. In his insightful article, he demonstrates an alternative approach using alignment. His solution elegantly tackles the parallax effect, and I highly recommend checking out his work for a deeper dive into this topic: Chris Banes – Parallax Effect in Jetpack Compose. It’s a testament to the flexibility and capabilities of Jetpack Compose in enabling creative visions on the Android platform.



This article was previously published on



It’s one of the common UX across apps to provide swipe to dismiss so…
In this part of our series on introducing Jetpack Compose into an existing project,…
In the world of Jetpack Compose, where designing reusable and customizable UI components is…

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.