accompanist: [Swipe to refresh] Indicator stuck on Android 12 stretch overscroll effect

Description If you stop the pull motion briefly and then resume the motion, the indicator will stay at the same place and the motion propagates down to the LazyColumn to trigger the overscroll effect.

Note: this does not happen with the legacy overscroll effect (prior to android 12)! It only happens with the “new” stretch overscroll effect.

This only happens if the motion is interrupted and the finger stays on the screen without any movement and then resumes.

Video:

https://user-images.githubusercontent.com/22662033/156323093-906214aa-40d6-4963-aa83-fb9e432fcbe4.mp4

Steps to reproduce

@Composable
fun PagerIssue() {
    var isRefreshing by remember { mutableStateOf(false) }

    SwipeRefresh(
        modifier = Modifier.fillMaxSize(),
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
    ) {
        LazyColumn(
            Modifier.fillMaxSize()
        ) {
            items(100) { index ->
                Text("I'm item Nr. $index")
            }
        }
    }

    LaunchedEffect(isRefreshing) {
        if (isRefreshing) {
            delay(1000)
            isRefreshing = false
        }
    }
}

Expected behavior Upon resume of the motion, the indication should continue it’s movement and continue the pull to refresh motion/action.

Additional context This problem can be worked around by wrapping the content of the SwipeRefresh composable with a CompositionLocalProvider which provides null as LocalOverScrollConfiguration while state.isSwipeInProgress is true:

@Composable
fun SwipeRefreshWithoutOverscroll(
    state: SwipeRefreshState,
    onRefresh: () -> Unit,
    modifier: Modifier = Modifier,
    swipeEnabled: Boolean = true,
    refreshTriggerDistance: Dp = 80.dp,
    indicatorAlignment: Alignment = Alignment.TopCenter,
    indicatorPadding: PaddingValues = PaddingValues(0.dp),
    indicator: @Composable (state: SwipeRefreshState, refreshTrigger: Dp) -> Unit = { s, trigger ->
        SwipeRefreshIndicator(s, trigger)
    },
    clipIndicatorToPadding: Boolean = true,
    content: @Composable () -> Unit,
) {
    SwipeRefresh(
        state = state,
        onRefresh = onRefresh,
        modifier = modifier,
        swipeEnabled = swipeEnabled,
        refreshTriggerDistance = refreshTriggerDistance,
        indicatorAlignment = indicatorAlignment,
        indicatorPadding = indicatorPadding,
        indicator = indicator,
        clipIndicatorToPadding = clipIndicatorToPadding,
    ) {
        val overscrollConfiguration = if (state.isSwipeInProgress) {
            null
        } else {
            LocalOverScrollConfiguration.current
        }
        CompositionLocalProvider(LocalOverScrollConfiguration provides overscrollConfiguration) {
            content()
        }
    }
}

Maybe this is also the solution to take as a PR? Personally i see no reason why the underlying scroll view should have any overscroll effect while isSwipeInProgress is true as the SwipeRefresh composable should handle all motion events.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 2
  • Comments: 15 (3 by maintainers)

Most upvoted comments