import androidx.compose.animation.core.* import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.size import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.gesture.DragObserver import androidx.compose.ui.gesture.dragGestureFilter import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.platform.AmbientDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlin.math.floor import android.graphics.Color as LegacyColor class PiecesValues(initial: Array>) { private var internalValues = initial fun getValueAt(row: Int, col: Int): String? { return internalValues[row][col] } fun getColorAt(row: Int, col: Int): Color? { return internalValues[row][col]?.let { getColor(it) } } fun setValueAt(row: Int, col: Int, value: String?) { internalValues[row][col] = value } private fun getColor(colorString: String): Color { return Color(LegacyColor.parseColor("#$colorString")) } } enum class GoingBackStates { INITIAL, FINAL } @Composable fun DragNDropWithBackTransition(sizePx: Float, modifier: Modifier = Modifier) { fun Float.getCoordFromAbsolute(): Int { return floor((this - sizePx * 0.055f) / (sizePx * 0.11f)).toInt() } var goBackAnimationStartX by remember { mutableStateOf(Float.NEGATIVE_INFINITY) } var goBackAnimationStartY by remember { mutableStateOf(Float.NEGATIVE_INFINITY) } var goBackAnimationEndX by remember { mutableStateOf(Float.NEGATIVE_INFINITY) } var goBackAnimationEndY by remember { mutableStateOf(Float.NEGATIVE_INFINITY) } val goBackAnimationDurationMillis = 1800 val goBackState = remember { MutableTransitionState(GoingBackStates.FINAL) } val goBackTransition = updateTransition(goBackState) val goBackAnimationX by goBackTransition.animateFloat( transitionSpec = { when { GoingBackStates.INITIAL isTransitioningTo GoingBackStates.FINAL -> { tween(durationMillis = goBackAnimationDurationMillis) } else -> { snap() } } } ) { state -> when (state) { GoingBackStates.INITIAL -> goBackAnimationStartX GoingBackStates.FINAL -> goBackAnimationEndX } } val goBackAnimationY by goBackTransition.animateFloat( transitionSpec = { when { GoingBackStates.INITIAL isTransitioningTo GoingBackStates.FINAL -> { tween(durationMillis = goBackAnimationDurationMillis) } else -> { snap() } } } ) { state -> when (state) { GoingBackStates.INITIAL -> goBackAnimationStartY GoingBackStates.FINAL -> goBackAnimationEndY } } var dndStartX by remember { mutableStateOf(Float.NEGATIVE_INFINITY) } var dndStartY by remember { mutableStateOf(Float.NEGATIVE_INFINITY) } var dndDestX by remember { mutableStateOf(Float.NEGATIVE_INFINITY) } var dndDestY by remember { mutableStateOf(Float.NEGATIVE_INFINITY) } val values by remember { mutableStateOf( PiecesValues( arrayOf( arrayOf("DCBA23", "123456", null, "CB23DE", null, null, "45C38F", null), arrayOf(null, null, null, "56E3DF", null, null, "45ED2C", null), arrayOf(null, null, null, "CB23DE", null, null, null, null), arrayOf("125687", null, null, null, null, null, null, null), arrayOf(null, "89DC2F", null, null, null, null, null, null), arrayOf(null, "123456", null, null, null, null, "DF52C9", null), arrayOf(null, null, null, null, null, "ABCDEF", null, null), arrayOf(null, "CAFEBABE", null, null, null, null, null, null), ) ) ) } val sizeDp = with(AmbientDensity.current) { sizePx.toDp() } return Canvas( modifier = modifier .size(sizeDp) .background(Color(LegacyColor.RED)) .dragGestureFilter( dragObserver = object : DragObserver { override fun onStart(downPosition: Offset) { val selectedCol = downPosition.x.getCoordFromAbsolute() if (selectedCol < 0 || selectedCol > 7) return val selectedRow = downPosition.y.getCoordFromAbsolute() if (selectedRow < 0 || selectedRow > 7) return values.getColorAt(row = selectedRow, col = selectedCol) ?: return dndStartX = downPosition.x dndStartY = downPosition.y dndDestX = downPosition.x dndDestY = downPosition.y } override fun onDrag(dragDistance: Offset): Offset { dndDestX += dragDistance.x dndDestY += dragDistance.y return dragDistance } override fun onStop(velocity: Offset) { val startCol = dndStartX.getCoordFromAbsolute() val startRow = dndStartY.getCoordFromAbsolute() val endCol = dndDestX.getCoordFromAbsolute() val endRow = dndDestY.getCoordFromAbsolute() if (endCol < 0 || endCol > 7 || endRow < 0 || endRow > 7) { onCancel() return } val startCellValue = values.getValueAt(col = startCol, row = startRow) val endCellValue = values.getValueAt(col = endCol, row = endRow) val destCellEmpty = endCellValue == null if (destCellEmpty) { values.setValueAt(col = startCol, row = startRow, value = null) values.setValueAt(col = endCol, row = endRow, value = startCellValue) } else { goBackAnimationStartX = dndDestX goBackAnimationStartY = dndDestY goBackAnimationEndX = dndStartX goBackAnimationEndY = dndStartY goBackState.targetState = GoingBackStates.FINAL } onCancel() } override fun onCancel() { dndStartX = Float.NEGATIVE_INFINITY dndStartY = Float.NEGATIVE_INFINITY dndDestX = Float.NEGATIVE_INFINITY dndDestY = Float.NEGATIVE_INFINITY } }, startDragImmediately = true, ) ) { val startCol = dndStartX.getCoordFromAbsolute() val startRow = dndStartY.getCoordFromAbsolute() val selectedCol = dndDestX.getCoordFromAbsolute() val selectedRow = dndDestY.getCoordFromAbsolute() (0 until 8).forEach { row -> (0 until 8).forEach { col -> val isDndStartCell = startCol == col && startRow == row val isDndDestCell = selectedCol == col && selectedRow == row val isDndCrossCell = selectedCol == col || selectedRow == row val whiteCell = (row + col) % 2 == 0 var color = if (whiteCell) Color(LegacyColor.WHITE) else Color(LegacyColor.BLACK) if (isDndStartCell) color = Color(LegacyColor.RED) if (isDndCrossCell) color = Color(LegacyColor.MAGENTA) if (isDndDestCell) color = Color(LegacyColor.GREEN) drawCell(size = sizeDp / 9, row = row, col = col, color = color) values.getColorAt(row, col)?.let { val isTheDraggedPiece = startCol == col && startRow == row val backAnimationInProgress = goBackState.currentState != GoingBackStates.FINAL val x = when { isTheDraggedPiece -> dndDestX backAnimationInProgress -> goBackAnimationX else -> sizePx * 0.11f * (1.0f + col) } val y = when { isTheDraggedPiece -> dndDestY backAnimationInProgress -> goBackAnimationY else -> sizePx * 0.11f * (1.0f + row) } drawItem(x, y, sizeDp / 9, it) } } } } } fun DrawScope.drawCell(size: Dp, row: Int, col: Int, color: Color) { val x = size.toPx() * (0.5f + col) val y = size.toPx() * (0.5f + row) drawRect(color = color, topLeft = Offset(x, y), size = Size(size.toPx(), size.toPx())) } fun DrawScope.drawItem(x: Float, y: Float, size: Dp, color: Color) { drawCircle(color = color, radius = size.toPx() * 0.5f, center = Offset(x, y)) } @Preview @Composable fun DragNDropWithBackTransitionPreview() { val sizePx = with(AmbientDensity.current) { 300.dp.toPx() } return DragNDropWithBackTransition(sizePx) }