让用户能够在屏幕上实际拖动组件是现代 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 包含以下主要部分:
- Anchors (
DraggableAnchors):一个构建器,用于定义自定义状态(例如Start, Center, End的枚举类)与其对应的像素偏移值之间的映射。 - State (
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 创建一个 2D 输入区域,其中选择器旋钮被限制在特定表面内。与可拖动元素示例(移动组件本身)不同,此实现使用 2D 增量在颜色选择器中移动子级可组合项“选择器”:
@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}
- 基于价值的动画