Возможность физически перетаскивать компоненты по экрану является фундаментальной для современных приложений 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) – построитель, который определяет сопоставление между вашими пользовательскими состояниями (например, класс enum для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(), чтобы сохранить текущую позицию компонента при повторном создании действия и изменении конфигурации (например, повороте экрана или изменении темы):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}
- Анимации на основе ценности