George Clensy
George Clensy

Reputation: 13

How can I put a LazyColumn in a Swipeable composable and have it swipe when the column is at the top

I'm building an Android launcher using Kotlin and Jetpack Compose, and I want to implement an app drawer similar to the Pixel Launcher. The drawer slides up using a LazyColumn to list the apps. I want the LazyColumn to allow scrolling, but if the user is at the top of the list, I want the swipe down gesture to close the drawer instead of bouncing the list.

The issue I'm facing is that the LazyColumn's scroll behavior is overriding the .swipeable() modifier on the Box. When the user is at the top of the LazyColumn, I want the swipe down gesture to close the drawer. However, the list just bounces instead of triggering the swipe action.

Here’s a minimal reproducible example of my current code:

@OptIn(ExperimentalWearMaterialApi::class)
@Composable
fun SwipeableHome() {
    val appDrawSize = 1000.dp
    val swipeableState = rememberSwipeableState(1)
    val sizePx = with(LocalDensity.current) { appDrawSize.toPx() }
    val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states
    val appsListScrollState = rememberLazyListState()

    Box(
        modifier = Modifier
            .fillMaxSize()
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                thresholds = { _, _ -> FractionalThreshold(0.3f) },
                orientation = Orientation.Vertical,
                enabled = isAppDrawAtTop.value
            )
    ) {
        Text("Home")
        AppsList()
    }
}

@Composable
fun AppsList() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .offset { IntOffset(0, swipeableState.offset.value.roundToInt()) }
    ) {
        LazyColumn(
            state = appsListScrollState,
            modifier = Modifier
                .offset { IntOffset(0, swipeableState.offset.value.roundToInt()) }
                .fillMaxHeight()
        ) {
            items(sortedInstalledApps) { app ->
               AppsListItem(app)
            }
        }
    }
}

I’ve tried using nestedScroll to detect when the user is swiping at the top of the list and disable the LazyColumn scrolling, but this results in requiring two swipes: the first swipe disables the scrolling, and the second closes the drawer.

Is there a better way to handle this interaction so that a swipe down at the top of the list closes the drawer seamlessly?


What I've Tried:

Here’s an example of my nested scroll attempt, which requires two swipes to close the draw:

val isAppDrawAtTop = remember {
    derivedStateOf { appsListScrollState.firstVisibleItemIndex == 0 && appsListScrollState.firstVisibleItemScrollOffset == 0 }
}
LazyColumn(
    state = appsListScrollState,
    modifier = Modifier
        .offset { IntOffset(0, swipeableState.offset.value.roundToInt()) }
        .fillMaxSize()
        .nestedScroll(remember {
            object : androidx.compose.ui.input.nestedscroll.NestedScrollConnection {
                override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                    return if (available.y > 0 && isAppDrawAtTop.value) {
                        shouldScroll.value = false
                        Offset.Zero
                    } else {
                        shouldScroll.value = true
                        super.onPreScroll(available, source)
                    }
                }
            }
        }),
    userScrollEnabled = shouldScroll.value
) {
    items(sortedInstalledApps) { app ->
        SwipeAppsListItem(app)
    }
}

Any help would be greatly appreciated. 👍

UPDATE: I did it using a horizontalPager

Upvotes: 1

Views: 157

Answers (0)

Related Questions