ã¯ããã«
ããã«ã¡ã¯ãã¬ã·ãäºæ¥é¨ã§ã¢ã«ãã¤ãä¸ã®æ¾æ¬ (@matsumo0922) ã§ããã¯ãã¯ãããã§ã¯ãä½ãã¬ã·ããæ¥ä»ãã¨ã«ç®¡çã§ããããã©ã³æ©è½ãã¤ãå æ¥ãªãªã¼ã¹ãã¾ããããã®æ©è½ã¯ Full-Compose ã§ä½æããã¦ãããæ¥ä»éã®ã¬ã·ãã®ç§»å/ä¸¦ã³æ¿ãã« Drag and Drop ãæ¡ç¨ãã¦ãã¾ããDrag and Drop ã¯æ¬æ¥ãªã¹ãã®ä¸¦ã³æ¿ãã«ç¨ãããã®ã§ã¯ç¡ãã§ãããä»ã ã« Compose ã§ã¯ãªã¹ãã®ä¸¦ã³æ¿ã API ãå å®ãã¦ããªãã®ã«å ããè¦è¦å¹æãè²§å¼±ãªãã®ãå¤ãã®ãç¾ç¶ã§ããããã§ Drag and Drop ãå¿ç¨çã«ä¸¦ã³æ¿ã UI ã«ç¨ãããã¨ã§ãç´æçã§è¦è¦çã«ããããããã UI/UX ãå®ç¾ãããã¨ãã§ãã¾ãããä»å㯠Compose ã§ Drag and Drop ãç¨ãã¦ããªã¹ãã®ä¸¦ã³æ¿ããå®è£ ããæ¹æ³ã¨ç¥è¦ããç´¹ä»ãã¾ãã

Drag and Drop ã®åºæ¬
Drag and Dropï¼ä»¥ä¸ DnDï¼ã¯ãã¦ã¼ã¶ã¼ãè¦ç´ ããã©ãã°ããå¥ã®ä½ç½®ã«ãããããããã¨ã§ãä¸¦ã³æ¿ããç§»åãªã©ã®æä½ãç´æçã«è¡ãã UI ãã¿ã¼ã³ã§ããåºç¾©ã§ã¯ View éãç»é¢éãã¢ããªéã§ã®ãã¼ã¿ã®ããåããå¯è½ãªæ©è½ã®ãã¨ãæãã¾ãããã®ããä¸è¬çãªãªã¹ããã¿ã¼ã³ã§ããã鏿ã¨ç§»åãã¨ã¯ç°ãªãæ©è½ã§ãããã¨ã«æ³¨æãå¿ è¦ã§ãããã©ã³æ©è½ã§ã¯ãããæ¥ä»ã«ç»é²ãããã¬ã·ãï¼ã¢ã¤ãã ï¼ãå¥ã®æ¥ä»ï¼ã»ã¯ã·ã§ã³ï¼ã¸è¦è¦çã«ç§»åãããå¿ è¦ããã£ããããé常ã®ä¸¦ã³æ¿ãã§ã¯ãªã DnD ãæ¡ç¨ãããã¨ã«ãã¾ããã
Compose ã§ã® DnD ã¯äºã¤ã®ä¿®é£¾åã§å®è£ ãããã¨ãã§ãã¾ãã
- Modifier.dragAndDropSource
- Modifier.dragAndDropTarget
ãããã Drag ã®èµ·ç¹ã¨ãªã Composable 㨠Drop å ã® Composable ãæãã¾ããä»åã¯ç°¡åã®ããã«äºã¤ã® Composable éã§ããã¹ããã¼ã¿ãããåãããå®è£ ãèãã¦ã¿ã¾ãã
ãªããæ¬è¨äºã®æçµç®ç㯠DnD ãç¨ãã¦ãªã¹ãã®ä¸¦ã³æ¿ã UI ãå®è£ ãããã¨ãªã®ã§ãåºæ¬ç㪠DnD ã®ä»çµã¿ãå®è£ ãçè§£ããã¦ããæ¹ã¯ãä¸¦ã³æ¿ãã¸ã®å¿ç¨ãã¾ã§èªã¿é£ã°ãã¦ããã ãã¦æ§ãã¾ããã
dragAndDropSource
ãã¼ã¿ã®éä¿¡å
ã¨ãªã Composable ã«ã¤ãã修飾åã§ããéä¿¡ããããã¼ã¿ã¯ããã¹ããç»åããã¤ããªãªã©è¤æ°ãã¼ãºãããã¨æãã¾ããããã¹ã¦ ClipData ã¯ã©ã¹ã§ã©ãããã¦éä¿¡ãã¾ããéä¿¡å
ããããããã«äºåã«ç¤ºãåããã label ãä»ãã¦ã¤ã³ã¹ã¿ã³ã¹ãçæããDragAndDropTransferData ãè¿ãã¦ããããã¨ã§ DnD ãã¹ã¿ã¼ããã¾ããlabel ã¯ã¦ã¼ã¶ã¼ã¸ã® Description ã¨ãã¦ãç¨ãããããã¨ã«æ³¨æãã¦ãã ãããä»å㯠âHello!â ã¨ããããã¹ããã¼ã¿ãéä¿¡ãã¦ã¿ã¾ãã
private const val LABEL = "DnD sample data for Cookpad." Box( modifier = Modifier .size(128.dp) .background(Color.Red) .dragAndDropSource { _ -> DragAndDropTransferData(ClipData.newPlainText(LABEL,"Hello!")) } )
ä¸è¨ã®ã³ã¼ãã§ãã©ãã°ã®æ¤ç¥ããã¹ã¦è¡ã£ã¦ããã¾ãããã©ãã°ã®ã¿ã¤ãã³ã°ãèªåã§ã³ã³ããã¼ã«ãããå ´åã¯ãdetectDrag... ãªã©ã® Modifier ã§èªåã§ Drag ãæ¤ç¥ããstartTransfer ãå¼ã³åºãã¦ããããã¨ã§ DnD ãéå§ãããã¨ãã§ãã¾ãã以ä¸ã®ä¾ã¯ãé·æ¼ãå¾ã®ãã©ãã°ã®ã¿ãæ¤ç¥ããä¾ã§ãããã® Composable èªä½ã Clickable ã§ããå ´åãªã©ã«æ´»èºãã¾ãã
Box(
modifier = Modifier
.size(128.dp)
.background(Color.Red)
.dragAndDropSource(
block = {
// clickable ã¨ä¸¡ç«ãããããã«ãé·æ¼ãå¾ã®ãã©ãã°ã®ã¿æ¤ç¥ãã
detectDragGesturesAfterLongPress(
onDrag = { _, _ ->
/* no-op */
},
onDragStart = { _ ->
val clipData = ClipData.newPlainText(MEAL_PLAN_DAD_ITEM_LABEL, id)
val data = DragAndDropTransferData(clipData)
startTransfer(data)
},
)
},
)
)
DnD ãéå§ããã¨ãããã©ã«ãã§ã¯å½è©²ã® Composable ãåéæã«ãããã®ãè¦è¦å¹æã¨ãã¦æä¾ããã¾ããããã夿´ãããå ´åã¯ãdrawDragDecoration ãã©ã¡ã¼ã¿ã®ã©ã ãå
ã§ DrawScope ãæä¾ããã¦ããã®ã§ããããç¨ãã¦ä»»æã®è¦è¦å¹æã«å¤æ´ãããã¨ãã§ãã¾ãã
dragAndDropTarget
ãã¼ã¿ãåä¿¡ãã Composable ã«ã¤ãã修飾åã§ããåãåãç¶æ
ã Boolean ã§è¿ã shouldStartDragAndDrop ã¨ãDragAndDropTarget ã¨ãã DnD ã®ç¶æ
ãåãåãã³ã¼ã«ããã¯ããã©ã¡ã¼ã¿ã«æå®ãã¾ããä»åã¯åãåã£ããã¼ã¿ããã®ã¾ã¾è¡¨ç¤ºããã®ã§ãonDrop() å
ã§å
ã»ã©ç¤ºãåããã Label ãã©ããã確èªããä¸ã§ãreceiveItem ã«ã»ãããã¦ãã¾ããè¿ãå¤ã¯ãã¼ã¿ãæ¶è²»ããå ´å㯠trueãæ¶è²»ããªãã£ãå ´å㯠false ãè¿ãã¾ãã
var receiveItem by remember { mutableStateOf("") } Box( modifier = Modifier .size(128.dp) .background(Color.LightGray) .dragAndDropTarget( shouldStartDragAndDrop = { true }, target = object : DragAndDropTarget { override fun onDrop(event: DragAndDropEvent): Boolean { val clip = event.toAndroidDragEvent().clipData val item = clip.getItemAt(0).text.toString() if (clip.description.label != LABEL) return false receiveItem = item return true } } ), contentAlignment = Alignment.Center ) { Text(receiveItem) }
DragAndDropTarget ã§ã¯ DnD ã®éå§ãçµäºãDrag ãåãåãå¯è½ç¯å²ã«å ¥ã£ããåºã¦ãã£ãããªã©ã®æ å ±ãåå¾ãããã¨ãã§ãã¾ãã詳ããã¯ããã¥ã¡ã³ããã覧ãã ããã
- onStarted: ãã©ãã°ãéå§ãããæã«å¼ã°ããããã®ã¿ã¼ã²ããããã¼ã¿ãåãå ¥ãå¯è½ããè¿ãã
- onEntered: ãã©ãã°é åã«å ¥ã£ãæã
- onMoved: é åå ã§ç§»åä¸ã
- onExited: é åããåºãæã
- onDrop: ãããããããæãããã§ãã¼ã¿ãåå¾ããã
- onEnded: ãã©ãã°æä½ãçµäºããæã
åºæ¬ã³ã¼ãå ¨ä½
åè¿°ã®ã³ã¼ããã¾ã¨ãã¦åããã¦ã¿ã¾ãã赤ã Box ã sourceãã°ã¬ã¼ã® Box ã target ã§ãã
var sendItem by remember { mutableStateOf("Hello!") } var receiveItem by remember { mutableStateOf("") } Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy( space = 128.dp, alignment = Alignment.CenterVertically, ) ) { // éä¿¡å´ Box( modifier = Modifier .size(128.dp) .background(Color.Red) .dragAndDropSource { _ -> DragAndDropTransferData(ClipData.newPlainText(LABEL, sendItem)) }, contentAlignment = Alignment.Center, ) { Text(sendItem) } // åä¿¡å´ Box( modifier = Modifier .size(128.dp) .background(Color.LightGray) .dragAndDropTarget( shouldStartDragAndDrop = { true }, target = object : DragAndDropTarget { override fun onDrop(event: DragAndDropEvent): Boolean { val clip = event.toAndroidDragEvent().clipData val item = clip.getItemAt(0).text.toString() receiveItem = item return true } } ), contentAlignment = Alignment.Center, ) { Text(receiveItem) } }

ãªãããªè¦è¦å¹æ
ä¸è¨ã®ã³ã¼ããæ¹è¯ãããã¨ã§è¦è¦çã«ã»ã¯ã·ã§ã³éã®ç§»åãå®ç¾ãããã¨ãã§ãã¾ããããããè¦è¦å¹æã¯æä½éã§ã¦ã¼ã¶ã¼ã«ã¨ã£ã¦åããããã UI ã«ãªã£ã¦ããã¨ã¯è¨ãåãã¾ãããããå°ããªãããªè¦è¦å¹æã欲ããã¨ããã§ãããã©ã³æ©è½ã§ã¯æ¥ä»æ¯ã«ã»ã¯ã·ã§ã³ãç¬ç«ãã¦ããã®ã§ããããããããæ¥ä»ãæ¡å¤§ & ãã¤ã©ã¤ããããã¨ã§ãããåããããã UI ãå®ç¾ã§ãã¾ãããä»å㯠DropTarget ã«æ¡å¤§ãã¦æ ç·ãã¤ãã¦ã¿ã¾ãã
åè¿°ã®éããDragAndDropTarget ã§ã¯ DnD ã®éå§ãçµäºãDrag ãåãåãå¯è½ç¯å²ã«å
¥ã£ããåºã¦ãã£ãããåå¾ãããã¨ãã§ããã®ã§ããããå©ç¨ãã¾ããisFocused ã¨ãã夿°ã§ Drop å¯è½æã«åãåãå´ã® Composable ã大ãããããã¦æ ç·ã表示ããããã«ãã¦ã¿ã¾ããããModifier ã®é©ç¨é åºã«æ³¨æãã¦ãã ããã
// Drop å¯è½é åã«å ¥ã£ã¦ããã var isFocused by remember { mutableStateOf(false) } val focusedScale by animateFloatAsState( targetValue = if (isFocused) 1.2f else 1f, label = "focusedScale", ) val focusedColor by animateColorAsState( targetValue = if (isFocused) Color.Red else Color.Transparent, label = "focusedColor", ) val dragAndDropTarget = remember { object : DragAndDropTarget { // Drop ããæ override fun onDrop(event: DragAndDropEvent): Boolean { val clip = event.toAndroidDragEvent().clipData val item = clip.getItemAt(0).text.toString() receiveItem = item isFocused = false return true } // Drop å¯è½é åã«å ¥ã£ãæ override fun onEntered(event: DragAndDropEvent) { isFocused = true } // Drop å¯è½é åããåºã¦è¡ã£ãæ override fun onExited(event: DragAndDropEvent) { isFocused = false } // DnD ãçµäºããæ override fun onEnded(event: DragAndDropEvent) { isFocused = false } } } // åä¿¡å´ Box( modifier = Modifier .size(128.dp) .graphicsLayer( scaleX = focusedScale, scaleY = focusedScale, ) .border( width = 2.dp, color = focusedColor, ) .background(Color.LightGray) .dragAndDropTarget( shouldStartDragAndDrop = { true }, target = dragAndDropTarget, ), contentAlignment = Alignment.Center, ) { Text(receiveItem) }

ãã®ããã«ãåã«ãã¼ã¿ãåãåãã ãã§ãªããã¡ãã£ã¨ããè¦è¦çãªãã£ã¼ãããã¯ãå ãããã¨ã§ãUX ãåä¸ããããã¨ãã§ãã¾ããã¹ãã¼ããã©ã³ãªã©ã®ã¿ããããã¤ã¹ã§ã¯ãèªåã®æã§ç»é¢ã®ä¸é¨ãé ãã¦ãã¾ããã¡ã§ãããã®ãããããããå ããåãå ¥ãå¯è½ã§ãï¼ãã¨ãªã¢ã¯ã·ã§ã³ãããã¨ã§ã¢ãã©ã¼ãã³ã¹ã®åä¸ãæä½ãã¹ã®é²æ¢ãã²ãã¦ã¯æä½ã¸ã®ç´å¾æã«ç¹ãããã¨ãã§ããã¯ãã§ãã
ç¹ã«ãã©ã³æ©è½ã®ãããªãç»é¢å ã«è¤æ°ã®ããããã¿ã¼ã²ããï¼æ¥ä»ï¼ãåå¨ããã±ã¼ã¹ã§ã¯ããã®ãããªç´°ãããªã¤ã³ã¿ã©ã¯ã·ã§ã³ãã¢ããªã®ä½¿ãå¿å°ã大ããå·¦å³ããããããã¾ããã
ä¸¦ã³æ¿ãã¸ã®å¿ç¨
ä¸è¨ã®ã³ã¼ããæ¹è¯ãããã¨ã§è¦è¦çã«ã»ã¯ã·ã§ã³éã®ç§»åãå®ç¾ãããã¨ãã§ãã¾ããããããç§ãã¡ãéçºãã¦ãããã©ã³æ©è½ã§ã¯æ¥ä»å ï¼ã»ã¯ã·ã§ã³å ï¼ã§ã®ã¢ã¤ãã ã®ä¸¦ã³æ¿ããå®ç¾ããå¿ è¦ãããã¾ããããããã㯠DnD ãæ´»ç¨ãããªã¹ãã®ä¸¦ã³æ¿ãã«ã¤ãã¦è§£èª¬ãã¦ããã¾ãã
ãµã³ãã«ãã¼ã¿ã¨ãã¦ãã©ã³æ©è½ã¨åãããã«ãã»ã¯ã·ã§ã³ã®ä¸ã«ã¢ã¤ãã ãä¿æããæ§é ãå®ç¾©ãã¾ãã
@Stable data class SectionData( val id: String, val label: String, val items: List<ItemData>, ) @Stable data class ItemData( val id: String, val label: String, ) val sections = remember { mutableStateListOf( SectionData( id = "section-1", label = "Section 1", items = listOf( ItemData("A", "Item A"), ItemData("B", "Item B"), ItemData("C", "Item C"), ) ), SectionData( id = "section-2", label = "Section 2", items = listOf( ItemData("D", "Item D"), ItemData("E", "Item E"), ItemData("F", "Item F"), ) ), SectionData( id = "section-3", label = "Section 3", items = listOf( ItemData("G", "Item G"), ItemData("H", "Item H"), ItemData("I", "Item I"), ) ) ) }
ãã®ãã¼ã¿ã表示ãã¦ã¿ã¾ããSection 㨠Item ã¨ãã Composable ãç¨æãã¾ãããSection å ¨åã DropTarget ã«ãã¦ãItem ã DragSource ã«ãã¾ããåè¿°ãã DnD ã®è¦è¦å¹æãåããã¦å®è£ ãã¦ã¿ã¾ãããã
@Composable private fun Section( sectionData: SectionData, modifier: Modifier = Modifier, ) { var isFocused by remember { mutableStateOf(false) } val focusedScale by animateFloatAsState( targetValue = if (isFocused) 1.05f else 1f, label = "focusedScale", ) val focusedColor by animateColorAsState( targetValue = if (isFocused) Color.Red else Color.Transparent, label = "focusedColor", ) val dragAndDropTarget = remember { object : DragAndDropTarget { override fun onDrop(event: DragAndDropEvent): Boolean { isFocused = false return true } override fun onEntered(event: DragAndDropEvent) { isFocused = true } override fun onExited(event: DragAndDropEvent) { isFocused = false } override fun onEnded(event: DragAndDropEvent) { isFocused = false } } } Column( modifier = modifier .graphicsLayer( scaleX = focusedScale, scaleY = focusedScale, ) .border( width = 2.dp, color = focusedColor, ) .dragAndDropTarget( shouldStartDragAndDrop = { true }, target = dragAndDropTarget, ), ) { Text( text = sectionData.label, style = MiseTheme.typography.titleSmall, ) sectionData.items.forEach { item -> Item( modifier = Modifier .fillMaxWidth() .dragAndDropSource { _ -> DragAndDropTransferData(ClipData.newPlainText(LABEL, item.id)) }, itemData = item, ) } } } @Composable private fun Item( itemData: ItemData, modifier: Modifier = Modifier, ) { Box( modifier = modifier.background(Color.LightGray, RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center ) { Text( text = itemData.label, style = MiseTheme.typography.bodyMedium, ) } }

ãã©ã³æ©è½ã®ãã㪠UI ãã§ãã¾ããããã®å®è£
ããã¼ã¹ã«ä¸¦ã³æ¿ãã®ãã¸ãã¯ã追å ãã¦ããã¾ãã
æåã«ãè¿°ã¹ãéããDnD ã¯æ¬æ¥ãªã¹ãã®ä¸¦ã³æ¿ããªã©ã«ç¨ãããã®ã§ã¯ãªããããItem ã® Index ãåå¾ã§ãã便å©ã¡ã½ãããªã©ã¯åå¨ãã¾ãããæä¾ãããã®ã¯ DragAndDropTarget ããå¾ããããã©ãã°ä¸åã³ããããããã座æ¨ã®ã¿ã§ãããã®åº§æ¨ããã¢ã¤ãã ãä¸¦ã³æ¿ããã¹ã Index ãè¨ç®ã«ãã£ã¦æ±ãããã¨ã«ãªãã¾ãã
DragAndDropTarget ããåå¾ã§ãã座æ¨ã¯ç»é¢å
¨ä½ããè¦ã座æ¨ãªã®ã§ãã»ã¯ã·ã§ã³èªä½ã®Y座æ¨ã¨Headerã®é«ããã¢ã¤ãã ã®é«ããã Index ãæ±ãããã¨ãã§ãã¾ããDropTarget ããã¿ãç¸å¯¾åº§æ¨ã§ã¯ãªãã®ã§æ³¨æãã¦ãã ãããComposableã®Y座æ¨ãé«ã㯠Modifier.onGloballyPositionedãModifier.onSizeChanged ããåå¾ãããã¨ãã§ãã¾ãã
fun computeSlotIndex(yLocalInParent: Float): Int { // DropTarget ã® Items Column ã®ç¸å¯¾åº§æ¨ã«è¨ç®ãç´ã val yLocal = yLocalInParent - currentHeaderHeight - currentParentTop if (yLocal < 0f) return 0 for (index in currentItems.indices) { val itemId = currentItems[index].id // onGloballyPositioned ã§åå¾ãã Bounds val bounds = currentRowBounds[itemId] ?: continue // ãã® Item ã®ç¸å¯¾åº§æ¨ val rowTop = bounds - currentHeaderHeight - currentParentTop val rowBottom = rowTop + currentRowHeight // ãã® Item å ã§ä¸é¨åã«ããã°ãã® Item ã®åã // ä¸é¨åã«ããã°ãã® Item ã®å¾ãã® Index ãè¿ã if (yLocal in rowTop..rowBottom) { return if ((rowBottom - rowTop) / 2 < yLocal) index + 1 else index } } return if (yLocal > currentItems.size * currentRowHeight) { currentItems.size } else { -1 } }
DragAndDropTarget 㯠remember ããã¦ãããããå¤å´ã®å¤æ°ã®å¤åãåãåããã¨ãã§ãã¾ãããããã§ rememberUpdatedState ãç¨ãã¦ææ°ã®å¤ãåãåããã¨ãã§ããããã«ãã¾ããremember ã« key ãè¨å®ãããã¨ã§ããã®åé¡ã¯è§£æ±ºã§ãã¾ãããå¤ã®æ´æ°ããããã³ã« Callback ãä½ãç´ãã¦ãã¾ããæåãä¸å®å®ã«ãªã£ã¦ãã¾ããã rememberUpdatedState ã使ç¨ãã¾ãã
@Composable private fun Section( sectionData: SectionData, onItemDropped: (itemId: String, index: Int) -> Unit, modifier: Modifier = Modifier, ) { var isFocused by remember { mutableStateOf(false) } var hoveredSlotIndex by remember { mutableIntStateOf(-1) } var parentTopInRoot by remember { mutableFloatStateOf(0f) } val rowBoundsInRoot = remember { mutableStateMapOf<Any, Float>() } var headerHeight by remember { mutableFloatStateOf(0f) } var rowHeight by remember { mutableFloatStateOf(0f) } // Callback ã®ä¸ã§ãææ°ã®å¤ãåãåããããã« UpdatedState å val currentItems by rememberUpdatedState(sectionData.items) val currentHeaderHeight by rememberUpdatedState(headerHeight) val currentParentTop by rememberUpdatedState(parentTopInRoot) val currentRowHeight by rememberUpdatedState(rowHeight) val currentRowBounds by rememberUpdatedState(rowBoundsInRoot) val currentOnItemDropped by rememberUpdatedState(onItemDropped) // ä¸ç¥ val dragAndDropTarget = remember { object : DragAndDropTarget { override fun onDrop(event: DragAndDropEvent): Boolean { val clip = event.toAndroidDragEvent().clipData val itemId = clip.getItemAt(0).text.toString() currentOnItemDropped.invoke(itemId, hoveredSlotIndex) isFocused = false return true } override fun onEntered(event: DragAndDropEvent) { isFocused = true } override fun onExited(event: DragAndDropEvent) { isFocused = false hoveredSlotIndex = -1 } override fun onEnded(event: DragAndDropEvent) { isFocused = false hoveredSlotIndex = -1 } override fun onMoved(event: DragAndDropEvent) { val yLocal = event.toAndroidDragEvent().y hoveredSlotIndex = computeSlotIndex(yLocal) } } } // ä¸ç¥ }
Section Composable ã®å¼æ°ã« onItemDropped ã¨ããã©ã ããæ¸¡ãããã«ãã¾ããããã®å
é¨ã§ä¸¦ã³æ¿ãã®ãã¸ãã¯ãè¨è¿°ãã¾ãã以ä¸ã§ã¯ãã¼ã«ã«ã§ä¸¦ã³æ¿ããããã«è¤éãªå¦çãè¡ã£ã¦ãã¾ãããå®éã«ã¯ API ãªã©ã«ä¸¦ã³æ¿ãæ
å ±ãéããã¨ãå¤ãããããã¾ããã
// ç§»åå ã®ã»ã¯ã·ã§ã³ã¨ã¢ã¤ãã ãæ¢ã val sourceSectionIndex = sections.indexOfFirst { sec -> sec.items.any { it.id == itemId } } if (sourceSectionIndex == -1) return@Section val sourceSection = sections[sourceSectionIndex] val movedItem = sourceSection.items.find { it.id == itemId } ?: return@Section // ç§»åå ã®ã»ã¯ã·ã§ã³ã®ã¤ã³ããã¯ã¹ãæ¢ã val targetSectionIndex = sections.indexOfFirst { it.id == section.id } if (targetSectionIndex == -1) return@Section // ãã¼ã¿ã®æ´æ°å¦ç // åãã»ã¯ã·ã§ã³å ã§ã®ç§»åããå¥ã®ã»ã¯ã·ã§ã³ã¸ã®ç§»åãã§åå² if (sourceSectionIndex == targetSectionIndex) { // ç§»åå ããåé¤ val currentItems = sourceSection.items.toMutableList() currentItems.remove(movedItem) // ç§»åå ã«æ¿å ¥ val safeIndex = index.coerceIn(0, currentItems.size) currentItems.add(safeIndex, movedItem) sections[sourceSectionIndex] = sourceSection.copy(items = currentItems) } else { // ç§»åå ããåé¤ val newSourceItems = sourceSection.items.toMutableList() newSourceItems.remove(movedItem) sections[sourceSectionIndex] = sourceSection.copy(items = newSourceItems) // ç§»åå ã«æ¿å ¥ val targetSection = sections[targetSectionIndex] val newTargetItems = targetSection.items.toMutableList() val safeIndex = index.coerceIn(0, newTargetItems.size) newTargetItems.add(safeIndex, movedItem) sections[targetSectionIndex] = targetSection.copy(items = newTargetItems) }
ããã§ã¢ã¤ãã ãä¸¦ã³æ¿ãããã¨ãã§ããããã«ãªãã¾ãããä»åã®å®è£
ã«ã¯å«ã¾ãã¦ãã¾ãããããã©ã³æ©è½ã§ã¯ãããã®å®è£
ã«å ããããããäºå®ã® Index ã«ãªã¬ã³ã¸è²ã®ç ´ç·ã表示ããã©ã®ã¢ã¤ãã ã®éã«é
ç½®ãããã®ãåããããããã¦ãã¾ããhoveredSlotIndex ã®ç®æã«ç·ã表示ããã ããªã®ã§ãå°ããªå®è£
ã³ã¹ãã§ UX ãæ¹åãããã¨ãã§ããããããã¾ããã
ä¸¦ã³æ¿ãã®æ©è½èªä½ã¯ããã§å®æã§ããããªã¹ãã®ä¸¦ã³æ¿ãUIã«ããã¦ããä¸ã¤å¤§äºãªæ©è½ãå®è£ ããå¿ è¦ãããã¾ãããã©ãã°ä½ç½®ã«å¿ããããªã¼ãã¹ã¯ãã¼ã«ã§ããRecyclerView ãªã©ã§æä¾ããã¦ããä¸¦ã³æ¿ã API ãå©ç¨ãã¦ããã¨ã©ããã¦ãå¿ããã¡ã§ããããªã¹ãã®ä¸¦ã³æ¿ããå®ç¾ãã以ä¸ç»é¢å¤ã¸ä¸¦ã³æ¿ããã¦ã¼ã¹ã±ã¼ã¹ãååã«æ³å®ããã¾ãããã¡ãã便å©ã¡ã½ãããªã©ã¯æä¾ããã¦ãã¾ããã®ã§ãèªåãã¡ã§å®è£ ããå¿ è¦ãããã¾ãã
åè¿°ã®éãããã©ãã°ä¸ã®åº§æ¨ã¯ DragAndDropTarget ããåå¾ã§ããã®ã§ãä½ç½®ã«å¿ãã¦é£ç¶ã§ã¹ã¯ãã¼ã« API ãå¼ã¹ã°è¯ãããã§ããSection å
ã«ä»¥ä¸ã®å¦çãæ¸ããã¨ã§ããã©ãã°ãèªèº«ã®ä¸ã«ããå ´åã«ã¹ã¯ãã¼ã«å¦çãæ
å½ãããããã«ãã¾ãããã®ãããSection 㨠Section ã®éã« padding ãªã©ããã£ãå ´åã¯ã¹ã¯ãã¼ã«ãéåãã¦ãã¾ãã®ã§æ³¨æãã¦ãã ããã
LaunchedEffect(isFocused, currentDragY) {
if (!isFocused || currentDragY == 0f) return@LaunchedEffect
val scrollThresholdPx = with(density) { 120.dp.toPx() }
val scrollAmount = 20f
val containerTop = scrollableContainerBounds.top + parentTopInRoot
val containerBottom = scrollableContainerBounds.bottom + parentTopInRoot
while (isActive) {
val dragPosition = currentDragY
when {
dragPosition < containerTop + scrollThresholdPx -> {
coroutineScope.launch {
scrollState.scrollBy(-scrollAmount)
}
}
dragPosition > containerBottom - scrollThresholdPx -> {
coroutineScope.launch {
scrollState.scrollBy(scrollAmount)
}
}
else -> break
}
delay(10)
}
}

ããã§ãªã¼ãã¹ã¯ãã¼ã«ãå®è£ ããæ¨æºã®ä¸¦ã³æ¿ã API ãªã©ã¨åç以ä¸ã®æåãå®è£ ãããã¨ãã§ãã¾ãã ð
ã¾ã¨ã
ä»å㯠Compose ã§ã® Darg and Drop ã®å®è£ ã¨ãªã¹ãä¸¦ã³æ¿ã UI ã¸ã®å¿ç¨ã«ã¤ãã¦ãç´¹ä»ãã¾ãããCompose ã Stable ã¨ãªã£ã¦5å¹´è¿ãçµéãã¾ããããªã¹ãã®ä¸¦ã³æ¿ã API ã¯å å®ãã¦ããªãã®ãç¾ç¶ã§ããDrag and Drop ã®æ©è½èªä½ã¯ä¸¦ã³æ¿ãã«é©ããç©ã§ã¯ããã¾ããããå¿ç¨æ¬¡ç¬¬ã§é«ã UX ãç¶æããã¾ã¾ä¸¦ã³æ¿ããå®è£ ãããã¨ãã§ããã®ã§ãåèã«ãªãã°ã¨æãã¾ãã
æå¾ã«ãã¯ãã¯ãããã§ã¯ç¾å¨çµ¶è³æ¡ç¨æ´»åä¸ã§ããæ¯æ¥ã®æçãæ¥½ãã¿ã«ãããçæ§ããã®ãå¿åããå¾ ã¡ãã¦ããã¾ãï¼






