New Inventory component.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-07-11 11:12:07 +02:00
parent 250d9cbb7c
commit 531e4bea98
25 changed files with 842 additions and 77 deletions

View file

@ -38,7 +38,7 @@ import com.pixelized.rplexicon.ui.screens.rolls.RollOverlay
import com.pixelized.rplexicon.ui.screens.rolls.RollOverlayViewModel
import com.pixelized.rplexicon.ui.screens.rolls.rememberBlurredRollOverlayHostState
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import dagger.hilt.android.AndroidEntryPoint
val NO_WINDOW_INSETS = WindowInsets(0, 0, 0, 0)
@ -128,7 +128,7 @@ class MainActivity : ComponentActivity() {
Snackbar(
modifier = Modifier
.padding(horizontal = 16.dp)
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
),

View file

@ -4,7 +4,6 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -35,7 +34,7 @@ import androidx.compose.ui.window.DialogProperties
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import com.pixelized.rplexicon.utilitary.extentions.toLabel
@Stable
@ -66,7 +65,7 @@ fun HandleHitPointEditDialog(
onDismissRequest = onDismissRequest,
) {
Surface(
modifier = Modifier.ddBorder(
modifier = Modifier.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class SkillEditDialogUio(
@ -48,7 +48,7 @@ fun HandleSkillEditDialog(
onDismissRequest = onDismissRequest,
) {
Surface(
modifier = Modifier.ddBorder(
modifier = Modifier.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -26,7 +26,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class SpellEditDialogUio(
@ -49,7 +49,7 @@ fun HandleSpellEditDialog(
onDismissRequest = onDismissRequest,
) {
Surface(
modifier = Modifier.ddBorder(
modifier = Modifier.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -2,7 +2,6 @@ package com.pixelized.rplexicon.ui.screens.character.composable.actions
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
@ -27,14 +26,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.model.Attack
import com.pixelized.rplexicon.ui.composable.images.AsyncImage
import com.pixelized.rplexicon.ui.screens.character.composable.common.DiceButton
import com.pixelized.rplexicon.ui.screens.character.composable.common.FlatValue
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.uri
@Stable
data class AttackUio(

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -26,7 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import com.pixelized.rplexicon.utilitary.extentions.toLabel
@Stable
@ -88,7 +87,7 @@ private fun StatPreview() {
Stat(
modifier = Modifier
.padding(all = 8.dp)
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
),

View file

@ -2,7 +2,6 @@ package com.pixelized.rplexicon.ui.screens.character.composable.dialogs
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -37,7 +36,7 @@ import com.pixelized.rplexicon.ui.composable.images.BackgroundImage
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.annotateWithDropCap
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class AlterationDialogDetailUio(
@ -75,7 +74,7 @@ fun AlterationDetailDialog(
enabled = false,
onClick = { },
)
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
),

View file

@ -34,7 +34,7 @@ import com.pixelized.rplexicon.ui.composable.images.BackgroundImage
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.annotateWithDropCap
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class SkillDialogDetailUio(
@ -70,7 +70,7 @@ fun SkillDetailDialog(
enabled = false,
onClick = { },
)
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
),

View file

@ -1,7 +1,6 @@
package com.pixelized.rplexicon.ui.screens.character.composable.dialogs
import android.content.res.Configuration
import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
@ -40,7 +39,7 @@ import com.pixelized.rplexicon.ui.composable.images.BackgroundImage
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.annotateWithDropCap
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class SpellDialogDetailUio(
@ -83,7 +82,7 @@ fun SpellDetailDialog(
enabled = false,
onClick = { },
)
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
),

View file

@ -0,0 +1,176 @@
package com.pixelized.rplexicon.ui.screens.character.pages.inventory
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.images.AsyncImage
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
data class InventoryItemUio(
val id: Int,
val name: String,
val amount: Int,
val container: Boolean,
val icon: Any?,
val items: List<InventoryItemUio> = emptyList(),
)
@Composable
fun InventoryItem(
modifier: Modifier = Modifier,
item: InventoryItemUio,
) {
Box(
modifier = Modifier
.then(other = modifier)
.aspectRatio(ratio = 1f),
) {
AsyncImage(
modifier = Modifier.matchParentSize(),
model = item.icon,
contentScale = ContentScale.Fit,
)
AnimatedContent(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.offset(x = 0.dp, y = 4.dp)
.padding(horizontal = 2.dp),
targetState = item.amount,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height / 2 } + fadeIn() togetherWith
slideOutVertically { height -> -height / 2 } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height / 2 } + fadeIn() togetherWith
slideOutVertically { height -> height / 2 } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
},
label = "Container count size",
) { amount ->
Text(
modifier = Modifier.graphicsLayer { this.alpha = if (amount == 1) 0f else 1f },
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.End,
text = amount.takeIf { it > 0 }?.let { "$it" } ?: " ", // &nbsp;
)
}
AnimatedContent(
modifier = Modifier
.align(alignment = Alignment.TopEnd)
.offset(x = 0.dp, y = (-4).dp)
.padding(horizontal = 2.dp),
targetState = item.items.size,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height / 2 } + fadeIn() togetherWith
slideOutVertically { height -> -height / 2 } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height / 2 } + fadeIn() togetherWith
slideOutVertically { height -> height / 2 } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
},
label = "Container count size",
) { amount ->
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.End,
text = amount.takeIf { it > 0 }?.let { "$it" } ?: " ", // &nbsp;
)
}
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, widthDp = 64)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 64)
private fun InventoryItemPreview(
@PreviewParameter(ClassInventoryItemProvider::class) preview: InventoryItemUio,
) {
LexiconTheme {
Surface {
InventoryItem(
item = preview,
)
}
}
}
private class ClassInventoryItemProvider : PreviewParameterProvider<InventoryItemUio> {
override val values: Sequence<InventoryItemUio> = sequenceOf(
InventoryItemUio(
id = 0,
name = "Pouch",
amount = 1,
container = true,
icon = R.drawable.icbg_pouch_a_unfaded,
),
InventoryItemUio(
id = 1,
name = "Scroll of blessing",
amount = 1,
container = false,
icon = R.drawable.icbg_scroll_of_bless_unfaded,
),
InventoryItemUio(
id = 2,
name = "Potion of blessing",
amount = 2,
container = false,
icon = R.drawable.icbg_pot_potion_of_healing_unfaded,
),
)
}

View file

@ -0,0 +1,254 @@
package com.pixelized.rplexicon.ui.screens.character.pages.inventory
import android.content.res.Configuration
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.DraggableItem
import com.pixelized.rplexicon.utilitary.dragContainer
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import com.pixelized.rplexicon.utilitary.rememberGridDragDropState
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
fun Pouet() {
val items = remember {
mutableStateOf(
listOf(
InventoryItemUio(
id = 0,
name = "Pouch",
amount = 1,
container = true,
icon = R.drawable.icbg_pouch_a_unfaded,
),
InventoryItemUio(
id = 1,
name = "Scroll of blessing",
amount = 1,
container = false,
icon = R.drawable.icbg_scroll_of_bless_unfaded,
),
InventoryItemUio(
id = 2,
name = "Scroll of spirit weapon",
amount = 1,
container = false,
icon = R.drawable.icbg_book_signedtradebisa_unfaded,
),
InventoryItemUio(
id = 3,
name = "Potion of healing",
amount = 2,
container = false,
icon = R.drawable.icbg_potion_of_superior_healing_unfaded,
),
InventoryItemUio(
id = 4,
name = "Potion of supérior healing",
amount = 2,
container = false,
icon = R.drawable.icbg_pot_potion_of_healing_unfaded,
),
InventoryItemUio(
id = 5,
name = "Potion of holy water",
amount = 2,
container = false,
icon = R.drawable.icbg_grn_holy_water_unfaded,
),
InventoryItemUio(
id = 6,
name = "Leather armor",
amount = 1,
container = false,
icon = R.drawable.icbg_leather_armour_rogue_unfaded,
),
InventoryItemUio(
id = 7,
name = "Silver battleaxe",
amount = 0,
container = false,
icon = R.drawable.icbg_battleaxe_plus_one_unfaded,
),
InventoryItemUio(
id = 8,
name = "Hand crossbow",
amount = 0,
container = false,
icon = R.drawable.icbg_hand_crossbow_unfaded,
),
InventoryItemUio(
id = 9,
name = "Goodberry",
amount = 0,
container = false,
icon = R.drawable.icbg_food_goodberry_unfaded,
),
InventoryItemUio(
id = 10,
name = "Goodberry",
amount = 0,
container = false,
icon = R.drawable.icbg_worg_fang_unfaded,
),
InventoryItemUio(
id = 11,
name = "Lantern of revealing",
amount = 0,
container = false,
icon = R.drawable.icbg_lantern_of_revealing,
),
InventoryItemUio(
id = 12,
name = "Dust of disappearance",
amount = 0,
container = false,
icon = R.drawable.icbg_haste_spore_grenade_unfaded,
),
InventoryItemUio(
id = 13,
name = "Pouch",
amount = 1,
container = true,
icon = R.drawable.icbg_pouch_a_unfaded,
),
)
)
}
val overIndex = remember { mutableIntStateOf(-1) }
val contentPadding = remember { PaddingValues(16.dp) }
val gridState = rememberLazyGridState()
val dragDropState = rememberGridDragDropState(
contentPadding = contentPadding,
gridState = gridState,
onMove = { fromIndex, toIndex ->
items.value = items.value.toMutableList().apply {
add(toIndex, removeAt(fromIndex))
}
},
onOver = { _, toIndex ->
val receiver = items.value[toIndex]
if (receiver.container) {
overIndex.intValue = toIndex
false
} else {
true
}
},
onDrop = { fromIndex, toIndex ->
if (fromIndex != toIndex) {
val receiver = items.value[toIndex]
if (receiver.container) {
items.value = items.value.toMutableList().apply {
val item = removeAt(fromIndex)
val receiverCopy = receiver.copy(
items = receiver.items.toMutableList().also {
it.add(item)
}
)
val receiverIndex = indexOf(receiver)
removeAt(receiverIndex)
add(receiverIndex, receiverCopy)
}
}
}
}
)
LexiconTheme {
Surface {
LazyVerticalGrid(
columns = GridCells.Fixed(5),
modifier = Modifier
.dragContainer(dragDropState)
.fillMaxSize(),
state = gridState,
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(
items = items.value,
key = { _, item -> item.id },
) { index, item ->
DraggableItem(
dragDropState = dragDropState,
index = index
) { isDragging, isOvering ->
val colorScheme = MaterialTheme.lexicon.colorScheme
val transition = updateTransition(
targetState = isDragging || isOvering ,
label = "Dragging transition",
)
val backgroundColor = transition.animateColor(
label = "Draggable item background color",
) {
val surface = colorScheme.base.surfaceColorAtElevation(2.dp)
when (it) {
true -> colorScheme.base.primary.copy(alpha = 0.15f)
.compositeOver(surface)
else -> surface
}
}
val outlineColor = transition.animateColor(
label = "Draggable item outline color",
) {
when (it) {
true -> colorScheme.base.primary
else -> colorScheme.characterSheet.outlineBorder
}
}
val innerColor = transition.animateColor(
label = "Draggable item inline color",
) {
when (it) {
true -> colorScheme.base.primary
else -> colorScheme.characterSheet.innerBorder
}
}
Box(
modifier = Modifier.doubleBorder(
backgroundColor = backgroundColor.value,
outline = remember { RoundedCornerShape(8.dp) },
outlineColor = outlineColor.value,
inner = remember { RoundedCornerShape(6.dp) },
innerColor = innerColor.value,
)
) {
InventoryItem(
item = item,
)
}
}
}
}
}
}
}

View file

@ -55,7 +55,7 @@ import com.pixelized.rplexicon.ui.screens.character.composable.character.StatUio
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterSheetStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.dialogs.SkillDetailDialog
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import kotlinx.coroutines.launch
@Stable
@ -146,7 +146,7 @@ fun ProficiencyPageContent(
Column(
modifier = Modifier
.sizeIn(minWidth = 100.dp, minHeight = 116.dp)
.ddBorder(outline = outline, inner = inner)
.doubleBorder(outline = outline, inner = inner)
.padding(all = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
@ -163,7 +163,7 @@ fun ProficiencyPageContent(
}
sheet.stats.forEach {
Stat(
modifier = Modifier.ddBorder(inner = inner, outline = outline),
modifier = Modifier.doubleBorder(inner = inner, outline = outline),
stat = it,
onClick = onStats,
)
@ -266,28 +266,28 @@ private fun ProficiencyLayout(
Column(
modifier = Modifier
.layoutId("SavingThrowsId")
.ddBorder(inner = inner, outline = outline),
.doubleBorder(inner = inner, outline = outline),
horizontalAlignment = Alignment.CenterHorizontally,
content = savingThrows,
)
Column(
modifier = Modifier
.layoutId("ProficienciesId")
.ddBorder(inner = inner, outline = outline),
.doubleBorder(inner = inner, outline = outline),
horizontalAlignment = Alignment.CenterHorizontally,
content = proficiencies,
)
Column(
modifier = Modifier
.layoutId("SpeedId")
.ddBorder(inner = inner, outline = outline),
.doubleBorder(inner = inner, outline = outline),
content = speed,
horizontalAlignment = Alignment.CenterHorizontally,
)
Column(
modifier = Modifier
.layoutId("PassivesId")
.ddBorder(inner = inner, outline = outline),
.doubleBorder(inner = inner, outline = outline),
content = passives,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,

View file

@ -55,7 +55,7 @@ import com.pixelized.rplexicon.isInDarkTheme
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import com.pixelized.rplexicon.utilitary.highlightRegex
import java.util.UUID
@ -118,7 +118,7 @@ fun ThrowsCard(
Surface(
modifier = modifier
.fillMaxWidth()
.ddBorder(
.doubleBorder(
inner = inner,
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -25,7 +25,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow
import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio
import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberAttributesSummary
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class AttributesSummaryUio(
@ -45,7 +45,7 @@ fun AttributesSummary(
) {
Column(
modifier = Modifier
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -26,7 +26,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow
import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio
import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberCharacteristicsSummary
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class CharacteristicsSummaryUio(
@ -48,7 +48,7 @@ fun CharacteristicsSummary(
) {
Column(
modifier = Modifier
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -25,7 +25,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow
import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio
import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberPassivesSummary
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class PassivesSummaryUio(
@ -43,7 +43,7 @@ fun PassivesSummary(
) {
Column(
modifier = Modifier
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -25,7 +25,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow
import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio
import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberProficienciesSummary
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class ProficiencySummaryUio(
@ -58,7 +58,7 @@ fun ProficiencySummary(
) {
Column(
modifier = Modifier
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -25,7 +25,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow
import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio
import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberSavingThrowsSummary
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class SavingThrowsSummaryUio(
@ -46,7 +46,7 @@ fun SavingThrowsSummary(
) {
Column(
modifier = Modifier
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -26,7 +26,7 @@ import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRow
import com.pixelized.rplexicon.ui.screens.summary.composable.common.SummaryRowUio
import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberSpellsSummary
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
@Stable
data class SpellSummaryUio(
@ -53,7 +53,7 @@ fun SpellSummary(
) {
Column(
modifier = Modifier
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -39,7 +39,7 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.screens.summary.composable.preview.statistic.rememberStatusSummary
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import com.pixelized.rplexicon.utilitary.extentions.modifier.verticalDivider
@Stable
@ -80,7 +80,7 @@ fun StatusSummary(
) {
Column(
modifier = Modifier
.ddBorder(
.doubleBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)

View file

@ -83,7 +83,7 @@ fun lightColorScheme(
secondary = BaseColorPalette.Teal80,
tertiary = BaseColorPalette.Teal40,
onPrimary = Color.White,
surfaceTint = BaseColorPalette.Purple40,
surfaceTint = Color.Gray,
),
placeholder: Color = Color(red = 230, green = 225, blue = 229),
sheet: LexiconColors.CharacterSheet = LexiconColors.CharacterSheet(

View file

@ -0,0 +1,287 @@
package com.pixelized.rplexicon.utilitary
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.zIndex
import com.pixelized.rplexicon.utilitary.lazyGridDragAndDrop.detectDragGesturesAfterLongPress
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/***
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt
*/
@Composable
fun rememberGridDragDropState(
contentPadding: PaddingValues,
gridState: LazyGridState,
onMove: (Int, Int) -> Unit,
onOver: (Int, Int) -> Boolean,
onDrop: (Int, Int) -> Unit,
): GridDragDropState {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val scope = rememberCoroutineScope()
val currentOnMove = rememberUpdatedState(newValue = onMove)
val currentOnOver = rememberUpdatedState(newValue = onOver)
val currentOnDrop = rememberUpdatedState(newValue = onDrop)
val state = remember(gridState, scope, currentOnMove) {
GridDragDropState(
contentOffset = with(density) {
Offset(
contentPadding.calculateTopPadding().toPx(),
contentPadding.calculateLeftPadding(layoutDirection).toPx()
)
},
scope = scope,
state = gridState,
onMove = currentOnMove.value,
onOver = currentOnOver.value,
onDrop = currentOnDrop.value,
)
}
LaunchedEffect(state) {
while (isActive) {
val diff = state.scrollChannel.receive()
gridState.scrollBy(diff)
}
}
return state
}
class GridDragDropState internal constructor(
private val contentOffset: Offset,
private val scope: CoroutineScope,
private val state: LazyGridState,
private val onMove: (Int, Int) -> Unit,
private val onOver: (Int, Int) -> Boolean,
private val onDrop: (Int, Int) -> Unit,
) {
internal val scrollChannel = Channel<Float>()
var draggingItemIndex by mutableStateOf<Int?>(null)
private set
var overItemIndex by mutableStateOf<Int?>(null)
private set
private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
internal val draggingItemOffset: Offset
get() = draggingItemLayoutInfo
?.let { item -> draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset() }
?: Offset.Zero
private val draggingItemLayoutInfo: LazyGridItemInfo?
get() = state.layoutInfo.visibleItemsInfo
.firstOrNull { it.index == draggingItemIndex }
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
private set
internal var previousItemOffset = Animatable(Offset.Zero, Offset.VectorConverter)
private set
internal fun onDragStart(offset: Offset) {
state.layoutInfo.visibleItemsInfo
.firstItemWithOffsetOrNull(offset = offset)
?.let {
draggingItemIndex = it.index
draggingItemInitialOffset = it.offset.toOffset()
}
}
internal fun onDragInterrupted(offset: Offset) {
val localDraggingItemIndex = draggingItemIndex
if (localDraggingItemIndex != null) {
state.layoutInfo.visibleItemsInfo
.firstItemWithOffsetOrNull(offset)
?.let { onDrop(localDraggingItemIndex, it.index) }
previousIndexOfDraggedItem = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
previousItemOffset.snapTo(startOffset)
previousItemOffset.animateTo(
Offset.Zero,
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Offset.VisibilityThreshold
)
)
previousIndexOfDraggedItem = null
}
}
overItemIndex = null
draggingItemDraggedDelta = Offset.Zero
draggingItemIndex = null
draggingItemInitialOffset = Offset.Zero
}
internal fun onDrag(offset: Offset) {
draggingItemDraggedDelta += offset
val draggingItem = draggingItemLayoutInfo ?: return
val startOffset = draggingItem.offset.toOffset() + draggingItemOffset
val endOffset = startOffset + draggingItem.size.toSize()
val middleOffset = startOffset + (endOffset - startOffset) / 2f
val middleXOffset = middleOffset.x.toInt()
val middleYOffset = middleOffset.y.toInt()
val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
draggingItem.index != item.index &&
item.offset.x <= middleXOffset && middleXOffset <= item.offsetEnd.x &&
item.offset.y <= middleYOffset && middleYOffset <= item.offsetEnd.y
}
overItemIndex = targetItem?.index
if (targetItem != null) {
if (onOver.invoke(draggingItem.index, targetItem.index)) {
onMove.invoke(draggingItem.index, targetItem.index)
draggingItemIndex = targetItem.index
}
} else {
val overscroll = when {
draggingItemDraggedDelta.y > 0 ->
(endOffset.y - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
draggingItemDraggedDelta.y < 0 ->
(startOffset.y - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
else -> 0f
}
if (overscroll != 0f) {
scrollChannel.trySend(overscroll)
}
}
}
private fun List<LazyGridItemInfo>.firstItemWithOffsetOrNull(
offset: Offset,
): LazyGridItemInfo? {
val offsetX = offset.x.toInt() - contentOffset.x.toInt()
val offsetY = offset.y.toInt() - contentOffset.y.toInt()
return this.firstOrNull { item ->
item.offset.x <= offsetX && offsetX <= item.offsetEnd.x &&
item.offset.y <= offsetY && offsetY <= item.offsetEnd.y
}
}
private val LazyGridItemInfo.offsetEnd: IntOffset
get() = this.offset + this.size
}
private operator fun IntOffset.plus(size: IntSize): IntOffset {
return IntOffset(x + size.width, y + size.height)
}
private operator fun Offset.plus(size: Size): Offset {
return Offset(x + size.width, y + size.height)
}
fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier {
return this then Modifier.pointerInput(dragDropState) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
dragDropState.onDrag(offset = offset)
},
onDragStart = { offset ->
dragDropState.onDragStart(offset)
},
onDragEnd = { offset ->
dragDropState.onDragInterrupted(offset)
},
onDragCancel = {
dragDropState.onDragInterrupted(Offset.Zero)
}
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyGridItemScope.DraggableItem(
dragDropState: GridDragDropState,
index: Int,
modifier: Modifier = Modifier,
content: @Composable (isDragging: Boolean, isOver: Boolean) -> Unit
) {
val dragging by remember(index, dragDropState) {
derivedStateOf {
index == dragDropState.draggingItemIndex
}
}
val overing by remember(index, dragDropState) {
derivedStateOf {
index == dragDropState.overItemIndex
}
}
val draggingModifier = when {
dragging -> {
Modifier
.zIndex(1f)
.graphicsLayer {
translationX = dragDropState.draggingItemOffset.x
translationY = dragDropState.draggingItemOffset.y
}
}
index == dragDropState.previousIndexOfDraggedItem -> {
Modifier
.zIndex(1f)
.graphicsLayer {
translationX = dragDropState.previousItemOffset.value.x
translationY = dragDropState.previousItemOffset.value.y
}
}
else -> {
Modifier.animateItemPlacement()
}
}
Box(
modifier = modifier.then(draggingModifier),
propagateMinConstraints = true,
) {
content(dragging, overing)
}
}

View file

@ -19,7 +19,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
@ -106,7 +105,7 @@ fun Modifier.clickableInterceptor(): Modifier = composed {
)
}
fun Modifier.ddBorder(
fun Modifier.doubleBorder(
horizontalSpacing: Dp = 3.dp,
verticalSpacing: Dp = 3.dp,
outline: Shape,
@ -114,37 +113,38 @@ fun Modifier.ddBorder(
inner: Shape,
innerWidth: Dp = 1.dp,
): Modifier = composed {
val isDarkTheme = isInDarkTheme()
val elevation = remember { derivedStateOf { if (isDarkTheme) 2.dp else 0.dp } }
val colorScheme = MaterialTheme.lexicon.colorScheme
this then Modifier
.border(
width = outlineWidth,
color = colorScheme.characterSheet.outlineBorder,
shape = outline,
)
.background(
shape = outline,
color = colorScheme.base.surfaceColorAtElevation(elevation.value)
)
.padding(
horizontal = horizontalSpacing,
vertical = verticalSpacing,
)
.border(
width = innerWidth,
color = colorScheme.characterSheet.innerBorder,
shape = inner,
)
.background(
shape = inner,
color = colorScheme.base.surfaceColorAtElevation(elevation.value)
)
.clip(
shape = inner,
)
this then Modifier.doubleBorder(
horizontalSpacing = horizontalSpacing,
verticalSpacing = verticalSpacing,
backgroundColor = colorScheme.base.surfaceColorAtElevation(1.dp),
outline = outline,
outlineWidth = outlineWidth,
outlineColor = colorScheme.characterSheet.outlineBorder,
inner = inner,
innerWidth = innerWidth,
innerColor = colorScheme.characterSheet.innerBorder,
)
}
fun Modifier.doubleBorder(
horizontalSpacing: Dp = 3.dp,
verticalSpacing: Dp = 3.dp,
backgroundColor: Color,
outline: Shape,
outlineWidth: Dp = 1.dp,
outlineColor: Color,
inner: Shape,
innerWidth: Dp = 1.dp,
innerColor: Color,
): Modifier = this then Modifier
.border(width = outlineWidth, color = outlineColor, shape = outline)
.background(shape = outline, color = backgroundColor)
.padding(horizontal = horizontalSpacing, vertical = verticalSpacing)
.border(width = innerWidth, color = innerColor, shape = inner)
.background(shape = inner, color = backgroundColor)
.clip(shape = inner)
fun Modifier.lexiconShadow(): Modifier {
return this then composed {
val isDarkTheme = isInDarkTheme()

View file

@ -0,0 +1,55 @@
package com.pixelized.rplexicon.utilitary.lazyGridDragAndDrop
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitLongPressOrCancellation
import androidx.compose.foundation.gestures.drag
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.CancellationException
suspend fun PointerInputScope.detectDragGesturesAfterLongPress(
onDragStart: (Offset) -> Unit = { },
onDragEnd: (Offset) -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
awaitEachGesture {
try {
// await for a press
val down = awaitFirstDown(requireUnconsumed = false)
// await for a long press or a cancellation
val drag = awaitLongPressOrCancellation(pointerId = down.id)
// if the user did a long press
if (drag != null) {
// callback onDragStart with the initial position.
onDragStart.invoke(drag.position)
var lastDragInput: PointerInputChange = drag
val dragEnded = drag(
pointerId = drag.id,
onDrag = { input ->
lastDragInput = input
onDrag(input, input.positionChange())
input.consume()
}
)
if (dragEnded) {
// consume up if we quit drag gracefully with the up
currentEvent.changes.fastForEach {
if (it.changedToUp()) it.consume()
}
onDragEnd(lastDragInput.position)
} else {
onDragCancel()
}
}
} catch (c: CancellationException) {
onDragCancel()
throw c
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB