当列表项在撰写中可拖动时,如何通过拖动打开导航抽屉?

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

所以我正在使用 Jetpack Compose 重写应用程序的 UI。我已经使用常规的 Scaffold 函数实现了一个 Navigation Drawer。开箱即用,这提供了两种打开抽屉的方法:按 navigationIcon 或拖动到屏幕末尾。有问题的屏幕是列表项的 LazyColumn。

我稍后在这些列表项上实施了 SwipeToDismiss 模式。滑动关闭可以正常工作,但无法再拖动到任何地方以打开导航抽屉。

在旧的基于视图的系统中,导航抽屉会保留一个小宽度,您可以随时在其中拖动以打开抽屉 - 无论子项目是否支持拖动。我不确定如何使用 Compose 实现相同的效果。看起来应该是导航抽屉的工作来处理这个 - 而不是它里面的屏幕。

带导航抽屉的屏幕:

    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
        ...
    }

并滑动以关闭项目(显示在 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) }
            )
        }
    }
}

您可以使用填充轻松减小可滑动范围,如下所示:


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) }
                )
            }
        }
    }
}

我不确定 Scaffold 是否应该负责,如果你认为它应该 - create an issue 关于撰写问题跟踪器

这是我在菲利普给出答案后最终使用的方法。它有点“不那么四四方方”。总之 - 关键是让父框处理点击 - 允许一个单独的框只关注滑动 - 而 feeditem 本身不处理点击


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()
                .onGloballyPositioned { layoutCoordinates ->
                    itemSize = layoutCoordinates.size.toSize()
                }
                .combinedClickable(
                    onLongClick = { ... },
                    onClick = { ... },
                )
        ) {
            Box(
                modifier = Modifier
                    .padding(start = 48.dp)
                    .matchParentSize()
                    .swipeable(
                        state = swipeableState,
                        anchors = anchors,
                        orientation = Orientation.Horizontal,
                        thresholds = { _, _ ->
                            FractionalThreshold(0.25f)
                        }
                    )
            )

            FeedItemPreview(
                item = "item",
                swipeableModifier = Modifier
                    .padding(start = 30.dp) // left distance for drawer
                    .onGloballyPositioned { layoutCoordinates ->
                        itemSize = layoutCoordinates.size.toSize()
                    }
                    .swipeable(
                        state = swipeableState,
                        anchors = anchors,
                        orientation = Orientation.Horizontal,
                        thresholds = { _, _ ->
                            FractionalThreshold(0.25f)
                        }
                    )
                ,
                modifier = Modifier
                    .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
            )
        }
    }
}

@Composable
fun FeedItemPreview(
    item: String,
    modifier: Modifier,
) {
    Text(
        item,
        modifier = modifier
    )
}

以应用程序中的示例为例,其中可滑动区域由边框突出显示: