fix: quick-actions empty state
This commit is contained in:
@@ -252,7 +252,7 @@ class HomeRootSection : Routes.Route() {
|
||||
FontFamily(Font(R.font.avenir_next_medium, FontWeight.Medium))
|
||||
}
|
||||
val selectedTiles = rememberAsyncMutableStateList(defaultValue = listOf()) {
|
||||
context.database.getQuickTiles()
|
||||
context.database.getQuickTiles().filter { it.isNotBlank() }
|
||||
}
|
||||
val latestUpdate by rememberAsyncMutableState(defaultValue = null) { Updater.latestRelease }
|
||||
var showQuickActionsMenu by remember { mutableStateOf(false) }
|
||||
@@ -450,367 +450,368 @@ class HomeRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
AnimatedContent(targetState = selectedTiles.isNotEmpty(), label = "QuickActionsTitleAnim") { hasQuickActions ->
|
||||
if (!hasQuickActions) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 4.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(50),
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 4.dp,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
AnimatedContent(targetState = selectedTiles.isNotEmpty(), label = "QuickActionsAnim") { hasQuickActions ->
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
if (!hasQuickActions) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 4.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(50),
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 4.dp,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
Text(
|
||||
translation["quick_actions_title"],
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(260.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Widgets,
|
||||
contentDescription = translation["quick_actions_icon_description"],
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = "No quick actions added yet",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Button(
|
||||
onClick = { showQuickActionsMenu = true },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = translation["add_quick_action_description"],
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(text = "Add")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
translation["quick_actions_title"],
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 4.dp)
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Start,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
translation["quick_actions_title"],
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Start,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(
|
||||
onClick = { showQuickActionsMenu = true },
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_manage),
|
||||
contentDescription = translation["manage_quick_actions_description"]
|
||||
)
|
||||
}
|
||||
FilterChip(
|
||||
selected = editMode,
|
||||
onClick = { editMode = !editMode },
|
||||
label = { Text(if (editMode) "Done" else "Edit") },
|
||||
leadingIcon = { Icon(Icons.Filled.DragHandle, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedTiles.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(260.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Widgets,
|
||||
contentDescription = translation["quick_actions_icon_description"],
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = "No quick actions added yet",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Button(
|
||||
onClick = { showQuickActionsMenu = true },
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = translation["add_quick_action_description"],
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(text = "Add")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val spacing = 6.dp
|
||||
var spanTick by remember { mutableIntStateOf(0) }
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = cardMargin)
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val baseCell = remember { (maxWidth - (spacing * 2)) / 3f }
|
||||
val baseCellPx = with(density) { baseCell.toPx() }
|
||||
|
||||
val (tilePositions, totalHeight) = remember(selectedTiles.size, spanTick) {
|
||||
val positions = mutableMapOf<String, Offset>()
|
||||
var currentX = 0f
|
||||
var currentY = 0f
|
||||
var rowMaxHeight = 0f
|
||||
val screenWidthPx = with(density) { maxWidth.toPx() }
|
||||
|
||||
selectedTiles.forEach { tileName ->
|
||||
cards.entries.find { entry -> entry.key.first == tileName }?.let {
|
||||
val card = it.key
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val tileWidthPx = with(density) { (baseCell * wSpan + spacing * (wSpan - 1)).toPx() }
|
||||
val tileHeightPx = with(density) { (baseCell * hSpan + spacing * (hSpan - 1)).toPx() }
|
||||
|
||||
if (currentX + tileWidthPx > screenWidthPx) {
|
||||
currentX = 0f
|
||||
currentY += rowMaxHeight
|
||||
rowMaxHeight = 0f
|
||||
}
|
||||
|
||||
positions[tileName] = Offset(currentX, currentY)
|
||||
currentX += tileWidthPx + with(density) { spacing.toPx() }
|
||||
if (tileHeightPx > rowMaxHeight) {
|
||||
rowMaxHeight = tileHeightPx
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showQuickActionsMenu = true },
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_manage),
|
||||
contentDescription = translation["manage_quick_actions_description"]
|
||||
)
|
||||
}
|
||||
FilterChip(
|
||||
selected = editMode,
|
||||
onClick = { editMode = !editMode },
|
||||
label = { Text(if (editMode) "Done" else "Edit") },
|
||||
leadingIcon = { Icon(Icons.Filled.DragHandle, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
positions to (currentY + rowMaxHeight)
|
||||
}
|
||||
val spacing = 6.dp
|
||||
var spanTick by remember { mutableIntStateOf(0) }
|
||||
|
||||
Box(modifier = Modifier.height(with(density) { totalHeight.toDp() })) {
|
||||
remember(selectedTiles.size, context.translation.loadedLocale) {
|
||||
selectedTiles.mapNotNull {
|
||||
cards.entries.find { entry -> entry.key.first == it }
|
||||
}
|
||||
}.forEach { (card, action) ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val _tick = spanTick
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val tileWidth = baseCell * wSpan + spacing * (wSpan - 1)
|
||||
val tileHeight = baseCell * hSpan + spacing * (hSpan - 1)
|
||||
val tileWidthPx = with(density) { tileWidth.toPx() }
|
||||
val tileHeightPx = with(density) { tileHeight.toPx() }
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = cardMargin)
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val baseCell = remember { (maxWidth - (spacing * 2)) / 3f }
|
||||
val baseCellPx = with(density) { baseCell.toPx() }
|
||||
|
||||
var offsetX by remember(card.first) { mutableStateOf(0f) }
|
||||
var offsetY by remember(card.first) { mutableStateOf(0f) }
|
||||
var isDragging by remember { mutableStateOf(false) }
|
||||
val (tilePositions, totalHeight) = remember(selectedTiles.size, spanTick) {
|
||||
val positions = mutableMapOf<String, Offset>()
|
||||
var currentX = 0f
|
||||
var currentY = 0f
|
||||
var rowMaxHeight = 0f
|
||||
val screenWidthPx = with(density) { maxWidth.toPx() }
|
||||
|
||||
LaunchedEffect(card.first, spanTick) {
|
||||
val (x, y) = getTileOffset(card.first)
|
||||
if (x != 0f || y != 0f) {
|
||||
offsetX = x
|
||||
offsetY = y
|
||||
} else {
|
||||
val pos = tilePositions[card.first]
|
||||
if (pos != null) {
|
||||
offsetX = pos.x
|
||||
offsetY = pos.y
|
||||
setTileOffset(card.first, offsetX, offsetY)
|
||||
selectedTiles.forEach { tileName ->
|
||||
cards.entries.find { entry -> entry.key.first == tileName }?.let {
|
||||
val card = it.key
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val tileWidthPx = with(density) { (baseCell * wSpan + spacing * (wSpan - 1)).toPx() }
|
||||
val tileHeightPx = with(density) { (baseCell * hSpan + spacing * (hSpan - 1)).toPx() }
|
||||
|
||||
if (currentX + tileWidthPx > screenWidthPx) {
|
||||
currentX = 0f
|
||||
currentY += rowMaxHeight
|
||||
rowMaxHeight = 0f
|
||||
}
|
||||
|
||||
positions[tileName] = Offset(currentX, currentY)
|
||||
currentX += tileWidthPx + with(density) { spacing.toPx() }
|
||||
if (tileHeightPx > rowMaxHeight) {
|
||||
rowMaxHeight = tileHeightPx
|
||||
}
|
||||
}
|
||||
}
|
||||
positions to (currentY + rowMaxHeight)
|
||||
}
|
||||
|
||||
val animatedOffsetX by animateFloatAsState(
|
||||
targetValue = offsetX,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "offsetX"
|
||||
)
|
||||
val animatedOffsetY by animateFloatAsState(
|
||||
targetValue = offsetY,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "offsetY"
|
||||
)
|
||||
Box(modifier = Modifier.height(with(density) { totalHeight.toDp() })) {
|
||||
remember(selectedTiles.size, context.translation.loadedLocale) {
|
||||
selectedTiles.mapNotNull {
|
||||
cards.entries.find { entry -> entry.key.first == it }
|
||||
}
|
||||
}.forEach { (card, action) ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val _tick = spanTick
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val tileWidth = baseCell * wSpan + spacing * (wSpan - 1)
|
||||
val tileHeight = baseCell * hSpan + spacing * (hSpan - 1)
|
||||
val tileWidthPx = with(density) { tileWidth.toPx() }
|
||||
val tileHeightPx = with(density) { tileHeight.toPx() }
|
||||
|
||||
val currentOffsetX = if (isDragging) offsetX else animatedOffsetX
|
||||
val currentOffsetY = if (isDragging) offsetY else animatedOffsetY
|
||||
var offsetX by remember(card.first) { mutableStateOf(0f) }
|
||||
var offsetY by remember(card.first) { mutableStateOf(0f) }
|
||||
var isDragging by remember { mutableStateOf(false) }
|
||||
|
||||
val baseModifier = Modifier
|
||||
.offset { IntOffset(currentOffsetX.roundToInt(), currentOffsetY.roundToInt()) }
|
||||
.width(tileWidth)
|
||||
.height(tileHeight)
|
||||
.padding(all = 6.dp)
|
||||
|
||||
val editModifier = baseModifier.then(
|
||||
Modifier.pointerInput(card.first, tileWidthPx, tileHeightPx) {
|
||||
var originalOffsetX = 0f
|
||||
var originalOffsetY = 0f
|
||||
detectDragGestures(
|
||||
onDragStart = {
|
||||
isDragging = true
|
||||
originalOffsetX = offsetX
|
||||
originalOffsetY = offsetY
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
offsetX += dragAmount.x
|
||||
offsetY += dragAmount.y
|
||||
},
|
||||
onDragEnd = {
|
||||
isDragging = false
|
||||
var targetTile: String? = null
|
||||
var maxOverlap = 0f
|
||||
val tileRect = Rect(Offset(offsetX, offsetY), Size(tileWidthPx, tileHeightPx))
|
||||
|
||||
for (otherTileName in selectedTiles) {
|
||||
if (otherTileName == card.first) continue
|
||||
val (otherOffsetX, otherOffsetY) = getTileOffset(otherTileName)
|
||||
val (otherWSpan, otherHSpan) = getTileSpan(otherTileName)
|
||||
val otherTileWidth = baseCell * otherWSpan + spacing * (otherWSpan - 1)
|
||||
val otherTileHeight = baseCell * otherHSpan + spacing * (otherHSpan - 1)
|
||||
val otherRect = Rect(Offset(otherOffsetX, otherOffsetY), Size(with(density) { otherTileWidth.toPx() }, with(density) { otherTileHeight.toPx() }))
|
||||
val intersectRect = tileRect.intersect(otherRect)
|
||||
val overlapArea = intersectRect.width * intersectRect.height
|
||||
if (overlapArea > maxOverlap) {
|
||||
maxOverlap = overlapArea
|
||||
targetTile = otherTileName
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTile != null) {
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val (targetWSpan, targetHSpan) = getTileSpan(targetTile!!)
|
||||
if (wSpan == targetWSpan && hSpan == targetHSpan) {
|
||||
// swap
|
||||
val (targetOffsetX, targetOffsetY) = getTileOffset(targetTile!!)
|
||||
setTileOffset(card.first, targetOffsetX, targetOffsetY)
|
||||
setTileOffset(targetTile!!, originalOffsetX, originalOffsetY)
|
||||
spanTick++ // this will trigger recomposition for all tiles
|
||||
} else {
|
||||
// revert
|
||||
offsetX = originalOffsetX
|
||||
offsetY = originalOffsetY
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(card.first, spanTick) {
|
||||
val (x, y) = getTileOffset(card.first)
|
||||
if (x != 0f || y != 0f) {
|
||||
offsetX = x
|
||||
offsetY = y
|
||||
} else {
|
||||
val pos = tilePositions[card.first]
|
||||
if (pos != null) {
|
||||
offsetX = pos.x
|
||||
offsetY = pos.y
|
||||
setTileOffset(card.first, offsetX, offsetY)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
val viewModifier = baseModifier.then(Modifier.scaleOnPress(interactionSource))
|
||||
|
||||
if (editMode) {
|
||||
ElevatedCard(
|
||||
modifier = editModifier
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 5.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = card.second, contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = card.first,
|
||||
lineHeight = 16.sp,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
// Drag handle for resizing
|
||||
var dxAccResize by remember(card.first, spanTick) { mutableStateOf(0f) }
|
||||
var dyAccResize by remember(card.first, spanTick) { mutableStateOf(0f) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(28.dp)
|
||||
.pointerInput(card.first, spanTick) {
|
||||
detectDragGestures(
|
||||
onDragStart = {
|
||||
dxAccResize = 0f
|
||||
dyAccResize = 0f
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
dxAccResize += dragAmount.x
|
||||
dyAccResize += dragAmount.y
|
||||
|
||||
var newW = wSpan
|
||||
var newH = hSpan
|
||||
val step = baseCellPx / 2f
|
||||
|
||||
while (dxAccResize > step) {
|
||||
newW = (wSpan + 1).coerceIn(1, 3)
|
||||
dxAccResize -= step
|
||||
}
|
||||
while (dxAccResize < -step) {
|
||||
newW = (wSpan - 1).coerceIn(1, 3)
|
||||
dxAccResize += step
|
||||
}
|
||||
while (dyAccResize > step) {
|
||||
newH = (hSpan + 1).coerceIn(1, 3)
|
||||
dyAccResize -= step
|
||||
}
|
||||
while (dyAccResize < -step) {
|
||||
newH = (hSpan - 1).coerceIn(1, 3)
|
||||
dyAccResize += step
|
||||
}
|
||||
|
||||
if (newW != wSpan || newH != hSpan) {
|
||||
setTileSpan(card.first, newW, newH)
|
||||
spanTick++
|
||||
selectedTiles.forEach { clearTileOffset(it) }
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Filled.DragHandle, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElevatedCard(
|
||||
modifier = viewModifier,
|
||||
onClick = { action(routes) },
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 5.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
|
||||
val animatedOffsetX by animateFloatAsState(
|
||||
targetValue = offsetX,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "offsetX"
|
||||
)
|
||||
val animatedOffsetY by animateFloatAsState(
|
||||
targetValue = offsetY,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "offsetY"
|
||||
)
|
||||
|
||||
val currentOffsetX = if (isDragging) offsetX else animatedOffsetX
|
||||
val currentOffsetY = if (isDragging) offsetY else animatedOffsetY
|
||||
|
||||
val baseModifier = Modifier
|
||||
.offset { IntOffset(currentOffsetX.roundToInt(), currentOffsetY.roundToInt()) }
|
||||
.width(tileWidth)
|
||||
.height(tileHeight)
|
||||
.padding(all = 6.dp)
|
||||
|
||||
val editModifier = baseModifier.then(
|
||||
Modifier.pointerInput(card.first, tileWidthPx, tileHeightPx) {
|
||||
var originalOffsetX = 0f
|
||||
var originalOffsetY = 0f
|
||||
detectDragGestures(
|
||||
onDragStart = {
|
||||
isDragging = true
|
||||
originalOffsetX = offsetX
|
||||
originalOffsetY = offsetY
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
offsetX += dragAmount.x
|
||||
offsetY += dragAmount.y
|
||||
},
|
||||
onDragEnd = {
|
||||
isDragging = false
|
||||
var targetTile: String? = null
|
||||
var maxOverlap = 0f
|
||||
val tileRect = Rect(Offset(offsetX, offsetY), Size(tileWidthPx, tileHeightPx))
|
||||
|
||||
for (otherTileName in selectedTiles) {
|
||||
if (otherTileName == card.first) continue
|
||||
val (otherOffsetX, otherOffsetY) = getTileOffset(otherTileName)
|
||||
val (otherWSpan, otherHSpan) = getTileSpan(otherTileName)
|
||||
val otherTileWidth = baseCell * otherWSpan + spacing * (otherWSpan - 1)
|
||||
val otherTileHeight = baseCell * otherHSpan + spacing * (otherHSpan - 1)
|
||||
val otherRect = Rect(Offset(otherOffsetX, otherOffsetY), Size(with(density) { otherTileWidth.toPx() }, with(density) { otherTileHeight.toPx() }))
|
||||
val intersectRect = tileRect.intersect(otherRect)
|
||||
val overlapArea = intersectRect.width * intersectRect.height
|
||||
if (overlapArea > maxOverlap) {
|
||||
maxOverlap = overlapArea
|
||||
targetTile = otherTileName
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTile != null) {
|
||||
val (wSpan, hSpan) = getTileSpan(card.first)
|
||||
val (targetWSpan, targetHSpan) = getTileSpan(targetTile!!)
|
||||
if (wSpan == targetWSpan && hSpan == targetHSpan) {
|
||||
// swap
|
||||
val (targetOffsetX, targetOffsetY) = getTileOffset(targetTile!!)
|
||||
setTileOffset(card.first, targetOffsetX, targetOffsetY)
|
||||
setTileOffset(targetTile!!, originalOffsetX, originalOffsetY)
|
||||
spanTick++ // this will trigger recomposition for all tiles
|
||||
} else {
|
||||
// revert
|
||||
offsetX = originalOffsetX
|
||||
offsetY = originalOffsetY
|
||||
}
|
||||
} else {
|
||||
setTileOffset(card.first, offsetX, offsetY)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
val viewModifier = baseModifier.then(Modifier.scaleOnPress(interactionSource))
|
||||
|
||||
if (editMode) {
|
||||
ElevatedCard(
|
||||
modifier = editModifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = card.second, contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = card.first,
|
||||
lineHeight = 16.sp,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 5.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = card.second, contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = card.first,
|
||||
lineHeight = 16.sp,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
// Drag handle for resizing
|
||||
var dxAccResize by remember(card.first, spanTick) { mutableStateOf(0f) }
|
||||
var dyAccResize by remember(card.first, spanTick) { mutableStateOf(0f) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(28.dp)
|
||||
.pointerInput(card.first, spanTick) {
|
||||
detectDragGestures(
|
||||
onDragStart = {
|
||||
dxAccResize = 0f
|
||||
dyAccResize = 0f
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
dxAccResize += dragAmount.x
|
||||
dyAccResize += dragAmount.y
|
||||
|
||||
var newW = wSpan
|
||||
var newH = hSpan
|
||||
val step = baseCellPx / 2f
|
||||
|
||||
while (dxAccResize > step) {
|
||||
newW = (wSpan + 1).coerceIn(1, 3)
|
||||
dxAccResize -= step
|
||||
}
|
||||
while (dxAccResize < -step) {
|
||||
newW = (wSpan - 1).coerceIn(1, 3)
|
||||
dxAccResize += step
|
||||
}
|
||||
while (dyAccResize > step) {
|
||||
newH = (hSpan + 1).coerceIn(1, 3)
|
||||
dyAccResize -= step
|
||||
}
|
||||
while (dyAccResize < -step) {
|
||||
newH = (hSpan - 1).coerceIn(1, 3)
|
||||
dyAccResize += step
|
||||
}
|
||||
|
||||
if (newW != wSpan || newH != hSpan) {
|
||||
setTileSpan(card.first, newW, newH)
|
||||
spanTick++
|
||||
selectedTiles.forEach { clearTileOffset(it) }
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Filled.DragHandle, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElevatedCard(
|
||||
modifier = viewModifier,
|
||||
onClick = { action(routes) },
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 5.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = card.second, contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = card.first,
|
||||
lineHeight = 16.sp,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user