I'm developing a screen in Jetpack Compose that combines two scrolling components: a PullToRefresh for vertical scrolling and an ImageSlider implemented using HorizontalPager for horizontal scrolling. The PullToRefresh is going to allow users to refresh the screen content by pulling down. However, I'm encountering an issue where the vertical scroll gesture for the PullToRefresh is not being detected. As a result, I'm unable to trigger the refresh operation by pulling down on the screen. The horizontal scrolling in the ImageSlider works as expected, but it seems to be interfering with the vertical scroll detection.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PullToRefreshContent(
isRefreshing: Boolean,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val pullToRefreshState = rememberPullToRefreshState()
Box(modifier = modifier.nestedScroll(nestedScrollConnection)) {
content()
// Refresh logic
LaunchedEffect(isRefreshing) {
if (isRefreshing) pullToRefreshState.startRefresh()
else pullToRefreshState.endRefresh()
}
// PullToRefresh UI
PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
@Composable
fun ImageSlider(images: List<Media?>) {
val pagerState = rememberPagerState(initialPage = 0) { images.size }
HorizontalPager(state = pagerState) { currentPage ->
val painter = rememberAsyncImagePainter(model = images[currentPage]?.posterPath)
Card {
Image(painter = painter, contentDescription = null)
}
}
}
To resolve this conflict, I tried to implement a custom NestedScrollConnection to detect the scroll direction by examining the y offset of the scroll event. I assumed that a positive y offset would indicate a downward scroll. However, this solution was not working. When monitoring the scroll events, I observed that the y offset value is consistently either 0.0 or -0.0, regardless of the actual scroll direction.
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
//Vertical scroll
return if (available.y > 0) {
//Do refresh operation
onRefresh()
pullToRefreshState.nestedScrollConnection.onPreScroll(available, source)
}
//Horizontal scroll
else {
Offset.Zero
}
}
}
}
First, make sure that you are using the material3 pulltorefresh
instead of the material2 pullrefresh
. Also check that you are using at least
implementation 'androidx.compose.material3:material3:1.3.0-beta05'
I found that you can use a combination of verticalScroll
and matchParentSize
Modifiers to get the desired functionality.
Please try out the following code:
@Composable
fun PullRefresh() {
var isRefreshing by remember { mutableStateOf(false) }
val state = rememberPullToRefreshState()
val coroutineScope = rememberCoroutineScope()
val onRefresh: () -> Unit = {
isRefreshing = true
coroutineScope.launch {
delay(1000)
isRefreshing = false
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Title") },
)
}
) {
PullToRefreshBox(
modifier = Modifier.fillMaxSize().padding(it),
state = state,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
) {
val pagerState = rememberPagerState { 3 }
HorizontalPager(
modifier = Modifier
.verticalScroll(rememberScrollState())
.matchParentSize(),
state = pagerState
) { currentPage ->
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "PAGE $currentPage")
}
}
}
}
}
The PullToRefreshBox
Composable needs a vertically scrollable child Composable. We make the HorizontalPager
vertically scrollable by applying the verticalScroll
Modifier.
However, with the verticalScroll
Modifier, fillMaxSize
is no longer working. Instead of fillMaxSize
, we can use the fillParentSize
Modifier that sets the size equal to the PullToRefreshBox
Composable which provides a BoxScope
.
Output: