androidnavigation-drawerandroid-jetpack-composeandroid-jetpack-compose-scaffold

How can Navigation Drawer be opened with a drag when a list item is draggable in compose?


So I am rewriting an app's UI using Jetpack Compose. I have implemented a Navigation Drawer using the regular Scaffold function. Out of the box this provides two ways of opening the drawer: either press the navigationIcon or drag towards End of screen. The screen in question is a LazyColumn of list items.

I have at a later date implemented the SwipeToDismiss pattern on these list items. The swipe to dismiss works fine but it is no longer possible to drag anywhere to open the navigation drawer.

In the old View-based system, the navigation drawer would reserve a small width inside which you could always drag to open the drawer - regardless of child items having drag support. I am unsure how to achieve the same using Compose. It seems like it should be the job of the navigation drawer to handle this - and not a screen inside it.

The screen with navigation drawer:

    val coroutineScope = rememberCoroutineScope()
    val scaffoldState = rememberScaffoldState(
        rememberDrawerState(initialValue = DrawerValue.Closed)
    )

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = { Text(screenTitle) },
                navigationIcon = {
                    IconButton(
                        onClick = {
                            coroutineScope.launch {
                                scaffoldState.drawerState.open()
                            }
                        }
                    ) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = "Drawer toggle button"
                        )
                    }
                },
                actions = {
                    ...
                }
            )
        },
        drawerContent = {
            // List of stuff
            ...
        },
        floatingActionButton = {
            ...
        }
    ) { padding ->
        /// Layout with a LazyColumn with elements having SwipeToDismiss
        ...
    }

and swipe to dismiss item (displayed inside LazyColumn)

@OptIn(
    ExperimentalFoundationApi::class,
    ExperimentalMaterialApi::class,
    ExperimentalAnimationApi::class
)
@Composable
fun SwipeableFeedItemPreview(
    onSwipe: suspend () -> Unit,
    onlyUnread: Boolean,
    item: FeedListItem,
    showThumbnail: Boolean,
    imagePainter: @Composable (String) -> Unit,
    onMarkAboveAsRead: () -> Unit,
    onMarkBelowAsRead: () -> Unit,
    onItemClick: () -> Unit
) {
    val animatedVisibilityState = remember { MutableTransitionState(true) }
    val swipeableState = rememberSwipeableState(initialValue = FeedItemSwipeState.NONE)
    // Needs to be set once layout is complete
    var itemSize by remember { mutableStateOf(Size(1f, 1f)) }
    val anchors = mapOf(
        0f to FeedItemSwipeState.NONE,
        -itemSize.width to FeedItemSwipeState.LEFT,
        itemSize.width to FeedItemSwipeState.RIGHT
    )

    AnimatedVisibility(
        visibleState = animatedVisibilityState,
        enter = fadeIn(1f),
        exit = shrinkVertically(Alignment.CenterVertically) + fadeOut()
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .onGloballyPositioned { layoutCoordinates ->
                    itemSize = layoutCoordinates.size.toSize()
                }
                .swipeable(
                    state = swipeableState,
                    anchors = anchors,
                    orientation = Orientation.Horizontal,
                    thresholds = { _, _ ->
                        FractionalThreshold(0.25f)
                    }
                )
        ) {
            Box(
                contentAlignment = swipeIconAlignment,
                modifier = Modifier
                    .matchParentSize()
                    .background(color)
                    .padding(horizontal = 24.dp)
            ) {
                AnimatedVisibility(
                    visible = swipeableState.targetValue != FeedItemSwipeState.NONE,
                    enter = fadeIn(),
                    exit = fadeOut()
                ) {
                    Icon(
                        when (item.unread) {
                            true -> Icons.Default.VisibilityOff
                            false -> Icons.Default.Visibility
                        },
                        contentDescription = stringResource(id = R.string.toggle_read_status)
                    )
                }
            }

            FeedItemPreview(
                item = item,
                showThumbnail = showThumbnail,
                imagePainter = imagePainter,
                onMarkAboveAsRead = onMarkAboveAsRead,
                onMarkBelowAsRead = onMarkBelowAsRead,
                onItemClick = onItemClick,
                modifier = Modifier
                    .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
            )
        }
    }
}

Solution

  • You can easily decrease swipeable range using padding, like this:

    
    enum class FeedItemSwipeState {
        NONE, LEFT, RIGHT,
    }
    
    @Composable
    fun TestView(
    ) {
        val scaffoldState = rememberScaffoldState(
            rememberDrawerState(initialValue = DrawerValue.Closed)
        )
    
        Scaffold(
            scaffoldState = scaffoldState,
            drawerContent = {
    
            },
        ) {
            val swipeableState = rememberSwipeableState(initialValue = FeedItemSwipeState.NONE)
            // Needs to be set once layout is complete
            var itemSize by remember { mutableStateOf(Size(1f, 1f)) }
            val anchors = mapOf(
                0f to FeedItemSwipeState.NONE,
                -itemSize.width to FeedItemSwipeState.LEFT,
                itemSize.width to FeedItemSwipeState.RIGHT
            )
            Box(
                modifier = Modifier
                    .fillMaxWidth()
            ) {
                Box(Modifier.fillMaxWidth()) {
                    Box(
                        modifier = Modifier
                            .matchParentSize()
                            .clickable { // clickable on whole view
                            }
                            .padding(start = 30.dp) // left distance for drawer
                            .onGloballyPositioned { layoutCoordinates ->
                                itemSize = layoutCoordinates.size.toSize()
                            }
                            .swipeable( // swipeable after padding to allow drawerContent work
                                state = swipeableState,
                                anchors = anchors,
                                orientation = Orientation.Horizontal,
                                thresholds = { _, _ ->
                                    FractionalThreshold(0.25f)
                                }
                            )
                    )
                    Text(
                        "item",
                        modifier = Modifier
                            .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                    )
                }
            }
        }
    }
    

    I'm not sure if that's something Scaffold should be responsible for, if you think it should - create an issue on compose issue tracker