中文 | English
An Android Jetpack Compose sample and small library module for handling same-direction nested HorizontalPager gestures.
It fixes the common case where an inner HorizontalPager reaches its first or last page and the gesture should continue into the parent HorizontalPager, but the parent page switch feels delayed, hard to fling, or can get stuck between pages.
The parent pager can lag at the child boundary and may stop between pages.
before_fix.mp4
Boundary drag and fling hand off to the parent pager more consistently.
after_fix.mp4
A single HorizontalPager receives the full drag delta and fling velocity:
user drag / release
-> Pager scrollable receives the full delta
-> PagerState.currentPageOffsetFraction changes continuously
-> flingBehavior receives the full velocity
-> Pager settles to the target page
With nested horizontal pagers, the parent usually sees only what the child leaves behind:
user drags inside the child Pager
-> child Pager handles or competes for the gesture first
-> only boundary leftover delta reaches the parent
-> leftover velocity may be consumed by the default pageNestedScrollConnection
-> parent Pager receives an incomplete gesture
This can cause:
- parent paging that does not follow the finger at the child boundary;
- fling gestures that are much harder to trigger than a normal single
HorizontalPager; - the parent pager getting stuck between two pages when the user repeatedly drags around the boundary.
The library treats each parent-child pager pair as an explicit hand-off boundary.
During drag:
- the child keeps the gesture while it can scroll;
- when the child reaches a boundary in the gesture direction, the connection moves the parent via
PagerState.dispatchRawDelta; - once the parent starts moving in the current gesture, later deltas stay with the parent until fling/settle finishes.
During fling:
- boundary-direction velocity is intercepted in
onPreFlingbefore the child pager can consume it; - the parent pager runs
animateScrollToPage; onPostFlingstill snaps the parent if it was left between pages.
For 3+ nested pagers, create one connection for every adjacent pair.
app/
Demo app with two-level and three-level nested HorizontalPager examples.
nested-horizontal-pager/
Library module containing the nested pager hand-off logic.
@Composable
fun NestedHorizontalPager(
state: PagerState,
parentState: PagerState? = null,
...
)
@Composable
fun NestedHorizontalPagerContent(
state: PagerState,
enabled: Boolean = true,
...
)Use NestedHorizontalPager instead of HorizontalPager for pagers that participate in same-direction nesting. Pass parentState only when the pager has a direct parent pager; root pagers can omit it.
Use NestedHorizontalPagerContent around leaf page content when that content contains its own horizontal scrollables, such as LazyRow. This isolates leftover content fling velocity so it does not bubble into the outer pager hand-off connection.
Advanced APIs are still available if you need manual wiring:
object NoOpNestedScrollConnection : NestedScrollConnection
@Composable
fun rememberNestedHorizontalPagerConnection(
parentState: PagerState,
childState: PagerState
): NestedScrollConnectionThe library is published on Maven Central. Make sure your project uses mavenCentral():
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}Then add the dependency:
dependencies {
implementation("io.github.codeideal:nested-horizontal-pager:1.1.0")
}val outerPagerState = rememberPagerState(pageCount = { outerTabs.size })
NestedHorizontalPager(
state = outerPagerState
) { outerPage ->
val innerPagerState = rememberPagerState(pageCount = { innerTabs.size })
NestedHorizontalPager(
state = innerPagerState,
parentState = outerPagerState
) { innerPage ->
NestedHorizontalPagerContent(state = innerPagerState) {
// leaf page content, including LazyRow or other horizontal scrollers
}
}
}NestedHorizontalPager(
state = middlePagerState,
parentState = outerPagerState
) {
NestedHorizontalPager(
state = innerPagerState,
parentState = middlePagerState
) {
NestedHorizontalPagerContent(state = innerPagerState) {
// leaf page content
}
}
}Run the sample app:
./gradlew :app:assembleDebugThe app contains:
- a two-level nested pager demo;
- a three-level nested pager demo;
- boundary drag and fling hand-off examples.
The current implementation targets the common case:
- horizontal pagers;
- LTR layout;
reverseLayout = false;- same-direction nested pagers.
If your pager uses RTL or reverseLayout = true, the gesture direction mapping needs to be adapted.
Both the app and library module build successfully:
./gradlew :app:assembleDebug
./gradlew :nested-horizontal-pager:assembleDebug