ユーザーが画面上でコンポーネントを物理的にドラッグできるようにすることは、最新の Android アプリの基本です。カスタム スライダー、スワイプして閉じるコンポーネント、インタラクティブなキャンバスのいずれを構築する場合でも、Jetpack Compose にはドラッグ イベントを簡単に処理できる包括的な API セットが用意されています。
Compose のドラッグ API は、生のタッチイベントの複雑さを抽象化します。pointerInput を使用してタッチ座標を手動で計算し、ジェスチャーのライフサイクルを管理する代わりに、Compose の状態管理システムとアニメーション システムに直接関連付けられた組み込みの修飾子を使用できます。
このページでは、各ドラッグ API の使用方法と使用するタイミングについて説明します。
適切なドラッグ API を選択する
ドラッグ可能なコンテンツの実装に使用できる API はいくつかあります。
- 標準のマテリアル コンポーネント(
SwipeToDismissBoxやナビゲーション ドロワーなど)を選択する場合: デフォルトのマテリアル 3 のスワイプまたはドラッグの動作(メールの削除、タスクのアーカイブ、サイドメニューやボトムシートのオープンなど)が必要な場合。カスタムの状態やレイアウトの管理を必要とせずに、組み込みの動作を提供します。 Modifier.draggableを選択するタイミング: 未加工のドラッグ 差分のみが必要な場合(カスタム スライダーをドラッグする場合など)。詳細については、draggable を使用して 1 つの軸に沿ってドラッグするをご覧ください。- 選択
Modifier.draggable2Dする場合: X 軸と Y 軸の両方を同時に連続して移動するコンポーネントが必要な場合(地図をパンしたり、ユーザーが画面上の任意の場所にドラッグできるフローティング アクション ボタンを作成したりする場合など)。詳細については、Modifier.draggable2Dを使用して 2 次元でドラッグするをご覧ください。
- 選択
Modifier.anchoredDraggableを選択する状況: 2 つ以上の個別の位置(ボトムシート、ナビゲーション ドロワー、スワイプして表示するアクションなど)の間でスナップする必要があるカスタム インタラクティブ コンポーネントを構築する場合。詳細については、anchoredDraggableを使用してアンカー ポイント間でコンポーネントをドラッグするをご覧ください。
draggable を使用して 1 つの軸に沿ってドラッグする
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 サーフェス
AnchoredDraggable API は、主に次の部分で構成されています。
- アンカー(
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()を使用して、アクティビティの再作成と構成の変更(画面の回転やテーマの変更など)にわたってコンポーネントの現在の位置を保持します。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() }
[省略可] オーバースクロールを統合します。
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 で 2 つのディメンションをドラッグする
draggable2D 修飾子を使用して、個々の UI 要素を X 軸と Y 軸の両方に沿って同時に移動します。
コンポーザブル要素をドラッグする
この例は、draggable2D の最も一般的なユースケースを示しています。ユーザーが UI 要素を選択して、親コンテナ内の任意の場所に再配置できるようにします。
@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}
- 値ベースのアニメーション