ã¯ããã«
ããã«ã¡ã¯ãã¬ã·ãäºæ¥é¨ã§ã¢ã«ãã¤ãä¸ã®æ¾æ¬ (@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 ã®åºæ¬
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 = {
detectDragGesturesAfterLongPress(
onDrag = { _, _ ->
},
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: ãã©ãã°æä½ãçµäºããæã
https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).dragAndDropTarget(kotlin.Function1,androidx.compose.ui.draganddrop.DragAndDropTarget
åºæ¬ã³ã¼ãå
¨ä½
åè¿°ã®ã³ã¼ããã¾ã¨ãã¦åããã¦ã¿ã¾ãã赤ã 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)
}
}
赤è²ã® Box ããç°è² Box ã¸ã® DnD
ãªãããªè¦è¦å¹æ
ä¸è¨ã®ã³ã¼ããæ¹è¯ãããã¨ã§è¦è¦çã«ã»ã¯ã·ã§ã³éã®ç§»åãå®ç¾ãããã¨ãã§ãã¾ããããããè¦è¦å¹æã¯æä½éã§ã¦ã¼ã¶ã¼ã«ã¨ã£ã¦åããããã UI ã«ãªã£ã¦ããã¨ã¯è¨ãåãã¾ãããããå°ããªãããªè¦è¦å¹æã欲ããã¨ããã§ãããã©ã³æ©è½ã§ã¯æ¥ä»æ¯ã«ã»ã¯ã·ã§ã³ãç¬ç«ãã¦ããã®ã§ããããããããæ¥ä»ãæ¡å¤§ & ãã¤ã©ã¤ããããã¨ã§ãããåããããã UI ãå®ç¾ã§ãã¾ãããä»å㯠DropTarget ã«æ¡å¤§ãã¦æ ç·ãã¤ãã¦ã¿ã¾ãã
åè¿°ã®éããDragAndDropTarget ã§ã¯ DnD ã®éå§ãçµäºãDrag ãåãåãå¯è½ç¯å²ã«å
¥ã£ããåºã¦ãã£ãããåå¾ãããã¨ãã§ããã®ã§ããããå©ç¨ãã¾ããisFocused ã¨ãã夿°ã§ Drop å¯è½æã«åãåãå´ã® Composable ã大ãããããã¦æ ç·ã表示ããããã«ãã¦ã¿ã¾ããããModifier ã®é©ç¨é åºã«æ³¨æãã¦ãã ããã
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 {
override fun onDrop(event: DragAndDropEvent): Boolean {
val clip = event.toAndroidDragEvent().clipData
val item = clip.getItemAt(0 ).text.toString()
receiveItem = item
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
}
}
}
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)
}
Drop å¯è½æã«è¦è¦å¹æã追å ãã
ãã®ããã«ãåã«ãã¼ã¿ãåãåãã ãã§ãªããã¡ãã£ã¨ããè¦è¦çãªãã£ã¼ãããã¯ãå ãããã¨ã§ã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
ãã©ã³æ©è½ã®ãã㪠UI ãã§ãã¾ããããã®å®è£
ããã¼ã¹ã«ä¸¦ã³æ¿ãã®ãã¸ãã¯ã追å ãã¦ããã¾ãã
æåã«ãè¿°ã¹ãéããDnD ã¯æ¬æ¥ãªã¹ãã®ä¸¦ã³æ¿ããªã©ã«ç¨ãããã®ã§ã¯ãªããããItem ã® Index ãåå¾ã§ãã便å©ã¡ã½ãããªã©ã¯åå¨ãã¾ãããæä¾ãããã®ã¯ DragAndDropTarget ããå¾ããããã©ãã°ä¸åã³ããããããã座æ¨ã®ã¿ã§ãããã®åº§æ¨ããã¢ã¤ãã ãä¸¦ã³æ¿ããã¹ã Index ãè¨ç®ã«ãã£ã¦æ±ãããã¨ã«ãªãã¾ãã
DragAndDropTarget ããåå¾ã§ãã座æ¨ã¯ç»é¢å
¨ä½ããè¦ã座æ¨ãªã®ã§ãã»ã¯ã·ã§ã³èªä½ã®Y座æ¨ã¨Headerã®é«ããã¢ã¤ãã ã®é«ããã Index ãæ±ãããã¨ãã§ãã¾ããDropTarget ããã¿ãç¸å¯¾åº§æ¨ã§ã¯ãªãã®ã§æ³¨æãã¦ãã ãããComposableã®Y座æ¨ãé«ã㯠Modifier.onGloballyPositionedãModifier.onSizeChanged ããåå¾ãããã¨ãã§ãã¾ãã
fun computeSlotIndex(yLocalInParent: Float ): Int {
val yLocal = yLocalInParent - currentHeaderHeight - currentParentTop
if (yLocal < 0f ) return 0
for (index in currentItems.indices) {
val itemId = currentItems[index].id
val bounds = currentRowBounds[itemId] ?: continue
val rowTop = bounds - currentHeaderHeight - currentParentTop
val rowBottom = rowTop + currentRowHeight
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 ) }
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)
}
DnD ãç¨ããä¸¦ã³æ¿ã
ããã§ã¢ã¤ãã ãä¸¦ã³æ¿ãããã¨ãã§ããããã«ãªãã¾ãããä»åã®å®è£
ã«ã¯å«ã¾ãã¦ãã¾ãããããã©ã³æ©è½ã§ã¯ãããã®å®è£
ã«å ããããããäºå®ã® 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 )
}
}
ãªã¼ãã¹ã¯ãã¼ã«æ©è½ä»ãã® DnD ãç¨ããä¸¦ã³æ¿ã
ããã§ãªã¼ãã¹ã¯ãã¼ã«ãå®è£
ããæ¨æºã®ä¸¦ã³æ¿ã API ãªã©ã¨åç以ä¸ã®æåãå®è£
ãããã¨ãã§ãã¾ãã ð
ã¾ã¨ã
ä»å㯠Compose ã§ã® Darg and Drop ã®å®è£
ã¨ãªã¹ãä¸¦ã³æ¿ã UI ã¸ã®å¿ç¨ã«ã¤ãã¦ãç´¹ä»ãã¾ãããCompose ã Stable ã¨ãªã£ã¦5å¹´è¿ãçµéãã¾ããããªã¹ãã®ä¸¦ã³æ¿ã API ã¯å
å®ãã¦ããªãã®ãç¾ç¶ã§ããDrag and Drop ã®æ©è½èªä½ã¯ä¸¦ã³æ¿ãã«é©ããç©ã§ã¯ããã¾ããããå¿ç¨æ¬¡ç¬¬ã§é«ã UX ãç¶æããã¾ã¾ä¸¦ã³æ¿ããå®è£
ãããã¨ãã§ããã®ã§ãåèã«ãªãã°ã¨æãã¾ãã
æå¾ã«ãã¯ãã¯ãããã§ã¯ç¾å¨çµ¶è³æ¡ç¨æ´»åä¸ã§ããæ¯æ¥ã®æçãæ¥½ãã¿ã«ãããçæ§ããã®ãå¿åããå¾
ã¡ãã¦ããã¾ãï¼
cookpad.careers
cookpad.careers