让用户能够在屏幕上实际拖动组件是现代 Android 应用的基础。无论您是构建自定义滑块、滑动关闭组件还是交互式画布,Jetpack Compose 都提供了一整套全面的 API,可轻松处理拖动事件。
Compose 拖动 API 抽象出了原始触摸事件的复杂性。
您可以使用直接与 Compose 的状态管理和动画系统绑定的内置修饰符,而无需使用pointerInput手动计算触摸坐标和管理手势
生命周期。
本页介绍了如何以及何时使用每个拖动 API。
选择正确的拖动 API
您可以使用多个 API 来实现可拖动的内容。
- 在以下情况下,请选择标准 Material 组件(例如
SwipeToDismissBox或抽屉式导航栏):您需要默认的 Material 3 滑动或拖动 行为(例如,删除电子邮件、归档任务或打开侧边 菜单 / 底部动作条)。它们提供内置行为,而无需自定义状态或布局管理。 - 在以下情况下,请选择
Modifier.draggable:您只需要原始拖动 增量(例如,拖动自定义滑块)。如需了解详情,请参阅 使用 draggable 沿单个轴拖动。- 在以下情况下,请选择
Modifier.draggable2D:您需要一个组件同时在 X 轴和 Y 轴上连续移动(例如,平移地图或创建用户可以在屏幕上任意位置拖动的悬浮操作按钮)。如需了解详情,请参阅 使用Modifier.draggable2D在二维空间中拖动。
- 在以下情况下,请选择
- 在以下情况下,请选择
Modifier.anchoredDraggable:您要构建需要在两个或多个离散 位置之间贴靠的 自定义交互式组件(例如,底部动作条、抽屉式导航栏或 滑动显示操作)。如需了解详情,请参阅使用anchoredDraggable在 锚点之间拖动组件。
使用 draggable 沿单个轴拖动
draggable 修饰符是单向拖动手势的高级入口点,并以像素为单位报告拖动距离。
请务必注意,此修饰符与 scrollable 类似,仅检测手势。您需要保持状态并在屏幕上表示该状态
,例如通过 offset 修饰符移动元素:
@Composable private fun DraggableText() { var offsetX by remember { mutableFloatStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" ) }
如果您需要控制整个拖动
手势,请考虑改用拖动手势检测器,方法是通过 pointerInput 修饰符。
@Composable private fun DraggableTextLowLevel() { Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() offsetX += dragAmount.x offsetY += dragAmount.y } } ) } }
使用 anchoredDraggable 在锚点之间拖动组件
借助 anchoredDraggable 修饰符,您可以在一组
预定义的位置(称为锚点)之间拖动组件。当用户释放拖动手势时,组件会停留在最近的锚点。anchoredDraggable 对于创建自定义底部动作条、复杂滑块或抽屉式导航栏面板等组件非常有用。
API Surface
AnchoredDraggable API 由以下主要部分组成:
- 锚点 (
DraggableAnchors):一个构建器,用于定义自定义状态(例如,Start, Center, End的枚举类)及其对应的像素偏移值之间的映射。 - 状态容器 (
AnchoredDraggableState<T>):一个状态容器,用于管理组件的当前位置(偏移)、其停留在的锚点及其拖动进度。应使用rememberSaveable和AnchoredDraggableState.Saver<T>创建它,以便在配置 更改后保留其状态。 - 手势修饰符 (
Modifier.anchoredDraggable):一个修饰符,用于检测沿Orientation的拖动手势,使用拖动增量更新状态,并处理轻拂手势,以便在释放时将组件动画化到最近的锚点。
示例:构建自定义 SwipeToDismiss 组件
此示例演示了如何构建自定义 SwipeToDismiss 组件(白色横幅),用户可以向左滑动以关闭该组件,也可以向右拖动以查看滚动回弹拉伸效果。
定义锚点值 。定义一个枚举,表示可拖动内容可以停留在的离散位置:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
实例化并保存
AnchoredDraggableState。使用rememberSaveable和AnchoredDraggableState.Saver()在 Activity 重新创建和配置更改(例如,屏幕旋转或主题更改)后持久保存组件的当前位置:val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
应用
layout、offset和draggable修饰符。如需使组件可拖动并确保其锚点适应其大小,请将以下修饰符应用于应在视觉上移动的内容:Modifier.layout:锚点位置通常取决于组件的大小,而在初始组合期间,组件的大小是未知的。使用layout修饰符测量组件,根据其宽度动态计算DraggableAnchors映射,并使用state.updateAnchors()更新状态。
Modifier.anchoredDraggable:连接到状态,在指定方向调度拖动增量,并处理轻拂手势,以便在释放时将组件动画化到最近的锚点。将此修饰符添加到应响应拖动手势的组件,并向其传递state和拖动orientation(以及可选的overscrollEffect)。Modifier.offset:从偏移 lambda 内的state.requireOffset()读取偏移。在此 lambda 中读取偏移可确保仅在放置阶段读取该值,从而避免在拖动期间进行不必要的重组。将此修饰符应用于应在视觉上移动的内容。
Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() }
[可选] 集成滚动回弹 。
overscrollEffectAPI 实现了一种视觉效果,当滚动 容器的边缘通过滚动或轻拂到达时,该效果会显示出来。当用户拖动超出定义的锚点边界时,您可以使用滚动回弹效果(通常是拉伸或发光)提供视觉反馈。- 调度:将
overscrollEffect实例传递给anchoredDraggable修饰符。这会将拖动增量调度到AnchoredDraggable状态或滚动回弹效果,具体取决于手势是在锚点边界内还是在锚点边界外。 - 渲染:将具有相同
OverscrollEffect实例的Modifier.overscroll应用于应在视觉上渲染 效果的组件。
- 调度:将
以下代码段展示了如何添加标准拉伸效果:
val overscrollEffect = rememberOverscrollEffect() Box( modifier.fillMaxWidth() ) { Box(Modifier.fillMaxWidth()) { background() } Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() } }
结果
上述代码段会生成一个白色横幅,其中包含文本“Drag me to dismiss!”,用户可以向左滑动以关闭该横幅(或向右拖动以查看滚动回弹拉伸效果):
示例:创建自定义手势驱动的导航抽屉式导航栏
此示例演示了如何构建自定义手势驱动的导航抽屉式导航栏。当用户水平拖动时,前景屏幕 (ScreenContents)
会向右滑动,从 1.0f 缩小到 0.8f,并将其边角进行圆角处理,以显示底层的导航抽屉式导航栏。
定义锚点状态和位置 。在此示例中,锚点是抽屉式导航栏的打开和关闭状态。为
Open和Closed状态定义枚举,并使用DraggableAnchors将它们映射到像素偏移:enum class DrawerState { Open, Closed } val anchors = DraggableAnchors { DrawerState.Open at drawerWidth DrawerState.Closed at 0f } val state = remember { AnchoredDraggableState( initialValue = DrawerState.Closed, anchors = anchors, ) }
在点击某个项时为
anchoredDraggableState添加动画效果。将HomeScreenDrawer放在Box的底部,并将ScreenContents放在顶部。抽屉式导航栏保持静止,而顶层在其上平移和缩放。当用户选择抽屉式导航栏项时,使用
state.animateTo(DrawerState.Closed)更新活动屏幕状态并自动关闭抽屉式导航栏:@Composable private fun AnchoredDraggableDrawerOnClickSample( modifier: Modifier = Modifier, state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { var screenState by remember { mutableStateOf(ScreenState.Home) } Box(modifier = modifier.fillMaxSize()) { HomeScreenDrawer( selectedScreen = screenState, onScreenSelected = { screen -> screenState = screen // Dismiss drawer when a destination is selected coroutineScope.launch { state.animateTo(DrawerState.Closed) } } ) // ScreenContents(...) } }
使用
state.requireOffset()更改多个视觉属性。 您可以使用多个属性,而不仅仅是偏移内容 ,Modifier.graphicsLayer以便根据拖动进度同时为平移 、缩放和圆角半径添加动画效果:- 使用
state.requireOffset()获取当前拖动偏移。 - 使用
lerp()(线性插值)计算相对于偏移和抽屉式导航栏宽度的比例缩放 (1f到0.8f) 和圆角半径 (0.dp到32.dp)。 Modifier.graphicsLayer在绘制阶段执行更改,确保高性能动画,而不会在拖动期间触发重组。
@Composable private fun AnchoredDraggableDrawerGraphicsLayerSample( state: AnchoredDraggableState<DrawerState>, drawerWidth: Float, selectedScreen: ScreenState, onDrawerClicked: () -> Unit ) { ScreenContents( selectedScreen = selectedScreen, onDrawerClicked = onDrawerClicked, modifier = Modifier .graphicsLayer { this.translationX = state.requireOffset() val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth) this.scaleX = scale this.scaleY = scale val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth) this.shape = RoundedCornerShape(cornerRadius.dp) this.clip = true } .anchoredDraggable(state, Orientation.Horizontal) ) }
- 使用
使用
animateTo触发程序化动画。通过在协程范围内启动state.animateTo(),以编程方式控制抽屉式导航栏(例如,在点按菜单图标或选择抽屉式导航栏项时):fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
结果
使用 Modifier.draggable2D 在二维空间中拖动
使用 draggable2D 修饰符可同时在 X 轴和 Y 轴上移动各个界面元素。
拖动可组合元素
此示例展示了 draggable2D 最常见的用例 - 允许用户拿起界面元素并将其重新放置在父容器内的任意位置。
@Composable private fun DraggableComposableElement() { // 1. Track the position of the floating window var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) { Box( modifier = Modifier // 2. Apply the offset to the box's position .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } // ... // 3. Attach the 2D drag logic .draggable2D( state = rememberDraggable2DState { delta -> // 4. Update the position based on the movement delta offset += delta } ), contentAlignment = Alignment.Center ) { Text("Video Preview", color = Color.White, fontSize = 12.sp) } } }
上述代码段包含以下内容:
- 使用
offset状态跟踪框的位置。 - 使用
offset修饰符根据拖动增量移动组件的位置。 - 由于没有轻拂支持,因此在用户抬起手指时,框会立即停止移动。
根据父级的拖动区域拖动子级可组合项
此示例演示了如何使用 draggable2D 创建二维输入区域,其中选择器旋钮受限于特定 surface。与移动组件本身的可拖动元素示例不同,此实现使用二维增量在颜色选择器上移动子级可组合项“选择器”:
@Composable private fun ExampleColorSelector( // ... ) { // 1. Maintain the 2D position of the selector in state. var selectorOffset by remember { mutableStateOf(Offset.Zero) } // 2. Track the size of the background container. var containerSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .size(300.dp, 200.dp) // Capture the actual pixel dimensions of the container when it's laid out. .onSizeChanged { containerSize = it } .clip(RoundedCornerShape(12.dp)) .background( brush = remember(hue) { // Create a simple gradient representing Saturation and Value for the given Hue. Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))) } ) ) { Box( modifier = Modifier .size(24.dp) .graphicsLayer { // Center the selector on the finger by subtracting half its size. translationX = selectorOffset.x - (24.dp.toPx() / 2) translationY = selectorOffset.y - (24.dp.toPx() / 2) } // ... // 3. Configure 2D touch dragging. .draggable2D( state = rememberDraggable2DState { delta -> // 4. Calculate the new position and clamp it to the container bounds val newX = (selectorOffset.x + delta.x) .coerceIn(0f, containerSize.width.toFloat()) val newY = (selectorOffset.y + delta.y) .coerceIn(0f, containerSize.height.toFloat()) selectorOffset = Offset(newX, newY) } ) ) } }
上述代码段包含以下内容:
- 它使用
onSizeChanged修饰符捕获渐变容器的实际尺寸。选择器确切知道边缘在哪里。 - 在
graphicsLayer内,它会调整translationX和translationY,以确保选择器在拖动时保持居中。
为你推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 了解手势
- 高级动画示例:手势 {:#gesture-and-animation}
- 基于价值的动画