Возможность физически перетаскивать компоненты по экрану является фундаментальной для современных приложений Android. Создаете ли вы собственный ползунок, компонент, который можно смахнуть, или интерактивный холст, Jetpack Compose предоставляет полный набор API для простой обработки событий перетаскивания.
API перетаскивания Compose абстрагируют сложность необработанных событий касания.
Вместо того чтобы вручную вычислять координаты касания и управлять жизненными циклами жестов с помощью pointerInput, вы можете использовать встроенные модификаторы, которые напрямую связаны с системами управления состоянием и анимации Compose.
На этой странице описано, как и когда использовать каждый 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(), чтобы сохранить текущую позицию компонента при повторном создании действия и изменении конфигурации (например, при повороте экрана или изменении темы):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: считывает смещение изstate.requireOffset()внутри лямбда-выражения смещения. Чтение смещения в этой лямбде гарантирует, что значение будет считываться только на этапе размещения, что позволяет избежать ненужных рекомпозиций во время перетаскивания. Примените этот модификатор к контенту, который должен визуально перемещаться.
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() }
Интегрируйте эффект достижения конца прокрутки (необязательно). API
overscrollEffectреализует визуальный эффект, который отображается, когда пользователь достигает краев прокручиваемого контейнера. Вы можете предоставить визуальную обратную связь, когда пользователь перетаскивает за пределы определенных границ привязки с эффектом прокрутки (обычно это растяжение или свечение).- Отправка. Передайте экземпляр
overscrollEffectв модификаторanchoredDraggable. Это отправляет дельты перетаскивания либо в состояниеAnchoredDraggable, либо в эффект достижения конца прокрутки, в зависимости от того, находится ли жест внутри или за пределами границ привязки. - Отрисовка. Примените
Modifier.overscrollс тем же экземпляромOverscrollEffectк компоненту, который должен визуально отрисовывать эффект.
- Отправка. Передайте экземпляр
В следующем фрагменте кода показано, как добавить стандартный эффект растягивания:
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для изменения положения компонента на основе дельт перетаскивания. - Поскольку поддержка прокрутки отсутствует, поле перестает двигаться в тот момент, когда пользователь поднимает палец.
Перетаскивание дочерней composable-функции на основе области перетаскивания родительской
В этом примере показано, как с помощью draggable2D создать двумерную область ввода, в которой ручка селектора ограничена определенной поверхностью. В отличие от примера с перетаскиваемым элементом, в котором перемещается сам компонент, в этой реализации используются двумерные дельты для перемещения дочернего селектора по палитре:
@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}
- Анимация на основе значений