Complementary article:
This is an app demonstrating the official Jetpack Compose Horizontal Pager.
This app shows how straightforward we can set up a Horizontal Pager, feed in whatever content we want, and apply animations.
No more custom views, adapters, fragments and complex lifecycle handling! Imagine how much extra work you need to build this using XML Views?
The page animations are all done using the graphicsLayer
modifier at the page composable. It calculates the offset of that specific page relative to the current active page, and applies transformations.
Card(
modifier = modifier
.graphicsLayer {
val pageOffset = (
(pagerState.currentPage - thisPageIndex) + pagerState
.currentPageOffsetFraction
)
alpha = lerp(
start = 0.4f,
stop = 1f,
fraction = 1f - pageOffset.absoluteValue.coerceIn(0f, 1f),
)
cameraDistance = 8 * density
rotationY = lerp(
start = 0f,
stop = 40f,
fraction = pageOffset.coerceIn(-1f, 1f),
)
lerp(
start = 0.5f,
stop = 1f,
fraction = 1f - pageOffset.absoluteValue.coerceIn(0f, 1f),
).also { scale ->
scaleX = scale
scaleY = scale
}
}
)
To make the page composable cleaner and not tied to the pager & animations, I have defined a custom Modifier.pagerAnimation()
which is equivalent to the code above. You can find it at com.rwmobi.composepager.ui.PagerAnimationModifier
.
To make the coupling looser, as the best practice, the PageLayout
composable has a modifier
parameter, so we only have to apply the pagerAnimation
modifier when calling it from the HorizontalPager()
, without a need to pass the pagerState
to the PageLayout
.
Let's try CompositionLocal
! We can perform haptic feedback in two lines of code.
The following LaunchedEffect
can perform haptic feedback during a page-change event. You may do some extra work related to the page change within the same collector.
var currentPageIndex by remember { mutableStateOf(0) }
val hapticFeedback = LocalHapticFeedback.current
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { currentPage ->
// This is required to avoid the trigger when the pager is first loaded
if (currentPageIndex != currentPage) {
hapticFeedback.performHapticFeedback(hapticFeedbackType = HapticFeedbackType.LongPress)
currentPageIndex = currentPage
}
// Anything to be triggered by page-change can be done here
}
}
The snapshotFlow
approach was recommended by the previous Accompanist documentation.
By manipulating the pagerState
, we can make the pager scroll endlessly. We simply multiply the original number of pages by a relatively large number, set the initialPage
to around the middle of the range, and then, when we need to resolve the index for contents, we take the remainder of the multiplied page index divided by the actual number of items, and we are good to go.
val endlessPagerMultiplier = 1000
val pageCount = endlessPagerMultiplier * drawables.size
val initialPage = pageCount / 2
val pagerState = rememberPagerState(
initialPage = initialPage,
initialPageOffsetFraction = 0f,
pageCount = { pageCount },
)
...
val resolvedPageContentIndex = absolutePageIndex % drawables.size
This project was configured to build using Android Studio Iguana | 2023.2.1. You will need to have Java 17 to build the project.
Alternatively, you can find the ready-to-install APKs and App Bundles under the release section.
- AndroidX Core KTX - Apache 2.0 - Extensions to Java APIs for Android development
- JUnit - EPL 2.0 - A simple framework to write repeatable tests
- AndroidX Test Ext JUnit - Apache 2.0 - Extensions for Android testing
- AndroidX Espresso - Apache 2.0 - UI testing framework
- AndroidX Lifecycle - Apache 2.0 - Lifecycles-aware components
- Jetpack Compose - Apache 2.0 - Modern toolkit for building native UI
- AndroidX Material3 - Apache 2.0 - Material Design components for Jetpack Compose
- Android Application Plugin - Google - Plugin for building Android applications
- Jetbrains Kotlin Android Plugin - JetBrains - Plugin for Kotlin Android projects
- Ktlint Plugin - JLLeitschuh - Plugin for Kotlin linter