Add snaping beavior to the LocationDetail

This commit is contained in:
Thomas Andres Gomez 2023-08-11 13:55:48 +02:00
parent 85cdd69570
commit e3cd0bdd4b
4 changed files with 133 additions and 72 deletions

View file

@ -30,14 +30,10 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.composable.AsyncImage
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.colors.LexiconColors
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.skydoves.landscapist.ImageOptions
import kotlin.math.sqrt
private val RADIUS = 12.dp
private val SQUARE = (RADIUS / sqrt(2f))
@Composable
fun FantasyMap(
@ -51,7 +47,7 @@ fun FantasyMap(
onMarquee: (MarqueeUio) -> Unit,
onTap: (Offset) -> Unit,
) {
val colorScheme = MaterialTheme.lexicon.colorScheme
val lexiconTheme = MaterialTheme.lexicon
val animatedScale = animateFloatAsState(
targetValue = state.scale,
@ -118,12 +114,11 @@ fun FantasyMap(
.onSizeChanged { state.imageSize = it }
.drawWithContent {
drawContent()
if (animatedMarqueeAlpha.value > 0f) {
item.value.marquees.forEachIndexed { index, item ->
if (item.position != Offset.Unspecified) {
drawMarque(
colorScheme = colorScheme,
theme = lexiconTheme,
alpha = animatedMarqueeAlpha,
scale = animatedScale,
position = item.position,
@ -134,7 +129,7 @@ fun FantasyMap(
}
if (highlight.value != Offset.Unspecified && animatedCrossAlpha.value > 0f) {
drawCross(
colorScheme = colorScheme,
theme = lexiconTheme,
alpha = animatedCrossAlpha,
scale = animatedScale,
position = highlight.value,
@ -257,34 +252,34 @@ class FantasyMapState(
}
private fun DrawScope.drawMarque(
colorScheme: LexiconColors,
theme: LexiconTheme,
alpha: State<Float>,
scale: State<Float>,
position: Offset,
selected: Boolean,
) {
val scaledRadius = theme.dimens.map.marqueeRadiusPx / scale.value
val scaledStroke = theme.dimens.map.marqueeStrokePx / scale.value
val scaledShadowDrop = theme.dimens.map.shadowDropPx / scale.value
drawCircle(
color = colorScheme.shadow,
color = theme.colorScheme.shadow,
alpha = alpha.value,
radius = 12.dp.toPx() / scale.value,
style = Stroke(
width = 2.dp.toPx() / scale.value,
),
radius = scaledRadius,
style = Stroke(width = scaledStroke),
center = Offset(
x = size.width * position.x,
y = size.height * position.y + 2.dp.toPx() / scale.value,
y = size.height * position.y + scaledShadowDrop,
)
)
drawCircle(
color = when (selected) {
true -> colorScheme.base.primary
true -> theme.colorScheme.base.primary
else -> Color.White
},
alpha = alpha.value,
radius = 12.dp.toPx() / scale.value,
style = Stroke(
width = 2.dp.toPx() / scale.value,
),
radius = scaledRadius,
style = Stroke(width = scaledStroke),
center = Offset(
x = size.width * position.x,
y = size.height * position.y,
@ -293,61 +288,65 @@ private fun DrawScope.drawMarque(
}
private fun DrawScope.drawCross(
colorScheme: LexiconColors,
theme: LexiconTheme,
alpha: State<Float>,
scale: State<Float>,
position: Offset,
) {
val scaledRadius = theme.dimens.map.crossRadiusPx / scale.value
val scaledStroke = theme.dimens.map.crossStrokePx / scale.value
val scaledShadowDrop = theme.dimens.map.shadowDropPx / scale.value
drawLine(
color = colorScheme.shadow,
color = theme.colorScheme.shadow,
alpha = alpha.value,
strokeWidth = 2.dp.toPx() / scale.value,
strokeWidth = scaledStroke,
start = Offset(
x = size.width * position.x - SQUARE.toPx() / scale.value,
y = size.height * position.y - SQUARE.toPx() / scale.value + 2.dp.toPx() / scale.value,
x = size.width * position.x - scaledRadius,
y = size.height * position.y - scaledRadius + scaledShadowDrop,
),
end = Offset(
x = size.width * position.x + SQUARE.toPx() / scale.value,
y = size.height * position.y + SQUARE.toPx() / scale.value + 2.dp.toPx() / scale.value,
x = size.width * position.x + scaledRadius,
y = size.height * position.y + scaledRadius + scaledShadowDrop,
)
)
drawLine(
color = colorScheme.shadow,
color = theme.colorScheme.shadow,
alpha = alpha.value,
strokeWidth = 2.dp.toPx() / scale.value,
strokeWidth = scaledStroke,
start = Offset(
x = size.width * position.x + SQUARE.toPx() / scale.value,
y = size.height * position.y - SQUARE.toPx() / scale.value + 2.dp.toPx() / scale.value,
x = size.width * position.x + scaledRadius,
y = size.height * position.y - scaledRadius + scaledShadowDrop,
),
end = Offset(
x = size.width * position.x - SQUARE.toPx() / scale.value,
y = size.height * position.y + SQUARE.toPx() / scale.value + 2.dp.toPx() / scale.value,
x = size.width * position.x - scaledRadius,
y = size.height * position.y + scaledRadius + scaledShadowDrop,
)
)
drawLine(
color = colorScheme.base.primary,
color = theme.colorScheme.base.primary,
alpha = alpha.value,
strokeWidth = 2.dp.toPx() / scale.value,
strokeWidth = scaledStroke,
start = Offset(
x = size.width * position.x - SQUARE.toPx() / scale.value,
y = size.height * position.y - SQUARE.toPx() / scale.value,
x = size.width * position.x - scaledRadius,
y = size.height * position.y - scaledRadius,
),
end = Offset(
x = size.width * position.x + SQUARE.toPx() / scale.value,
y = size.height * position.y + SQUARE.toPx() / scale.value,
x = size.width * position.x + scaledRadius,
y = size.height * position.y + scaledRadius,
)
)
drawLine(
color = colorScheme.base.primary,
color = theme.colorScheme.base.primary,
alpha = alpha.value,
strokeWidth = 2.dp.toPx() / scale.value,
strokeWidth = scaledStroke,
start = Offset(
x = size.width * position.x + SQUARE.toPx() / scale.value,
y = size.height * position.y - SQUARE.toPx() / scale.value,
x = size.width * position.x + scaledRadius,
y = size.height * position.y - scaledRadius,
),
end = Offset(
x = size.width * position.x - SQUARE.toPx() / scale.value,
y = size.height * position.y + SQUARE.toPx() / scale.value,
x = size.width * position.x - scaledRadius,
y = size.height * position.y + scaledRadius,
)
)
}

View file

@ -53,6 +53,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalSnack
@ -60,8 +61,10 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.Handle
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.rememberTextSize
import com.skydoves.landscapist.ImageOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -86,34 +89,21 @@ fun LocationDetail(
val scroll = rememberScrollState()
val pager = rememberPagerState()
val fantasy = rememberFantasyMapState()
val snapBehavior = rememberSnapConnection(scrollState = scroll)
val scrollBehavior = rememberScrollConnection(scrollState = scroll)
val ok = stringResource(id = android.R.string.ok)
val job = remember { mutableStateOf<Job?>(null) }
val connection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset = runBlocking {
Offset(
x = 0f,
y = when (scroll.canScrollForward) {
true -> -scroll.scrollBy(-available.y)
else -> 0f
},
)
}
}
}
val snackJob = remember { mutableStateOf<Job?>(null) }
val mapHighlight = remember { mutableStateOf(Offset.Unspecified) }
val selectedIndex = remember { mutableStateOf(0) }
Surface {
LocationContent(
modifier = Modifier.fillMaxSize(),
connection = connection,
modifier = Modifier
.fillMaxSize()
.nestedScroll(connection = snapBehavior),
connection = scrollBehavior,
scrollState = scroll,
pagerState = pager,
fantasyMapState = fantasy,
@ -131,8 +121,8 @@ fun LocationDetail(
}
},
onMapTap = {
job.value?.cancel()
job.value = scope.launch {
snackJob.value?.cancel()
snackJob.value = scope.launch {
snack.showSnackbar(
message = "x:${it.x}, y:${it.y}",
actionLabel = ok,
@ -146,7 +136,7 @@ fun LocationDetail(
if (it) {
mapHighlight.value = Offset.Unspecified
} else {
job.value?.cancel()
snackJob.value?.cancel()
}
fantasy.toggleFreeHand(it)
},
@ -388,6 +378,52 @@ private fun HandlePagerScroll(
}
}
@Composable
@Stable
private fun rememberSnapConnection(
scope: CoroutineScope = rememberCoroutineScope(),
scrollState: ScrollState,
): NestedScrollConnection {
val dimens = MaterialTheme.lexicon.dimens
return remember(scope, scrollState) {
object : NestedScrollConnection {
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
if (scrollState.value < dimens.map.mapSnapPx) {
scope.launch { scrollState.animateScrollTo(0) }
}
if ((scrollState.maxValue - scrollState.value) < dimens.map.mapSnapPx) {
scope.launch { scrollState.animateScrollTo(scrollState.maxValue) }
}
return super.onPostFling(consumed, available)
}
}
}
}
@Composable
@Stable
private fun rememberScrollConnection(
scrollState: ScrollState,
): NestedScrollConnection {
return remember(scrollState) {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset = runBlocking {
Offset(
x = 0f,
y = when (scrollState.canScrollForward) {
true -> -scrollState.scrollBy(-available.y)
else -> 0f
},
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import com.pixelized.rplexicon.ui.theme.animation.LexiconAnimation
@ -42,7 +43,8 @@ fun LexiconTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val lexiconTheme = remember {
val density = LocalDensity.current
val lexiconTheme = remember(density) {
LexiconTheme(
animation = lexiconAnimation(),
colorScheme = when (darkTheme) {
@ -50,7 +52,7 @@ fun LexiconTheme(
else -> lightColorScheme()
},
shapes = lexiconShapes(),
dimens = lexiconDimen(),
dimens = lexiconDimen(density = density),
typography = lexiconTypography(),
)
}

View file

@ -3,8 +3,10 @@ package com.pixelized.rplexicon.ui.theme.dimen
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.sqrt
@Stable
@Immutable
@ -12,7 +14,8 @@ data class LexiconDimens(
val item: Dp,
val detailPadding: Dp,
val itemListPadding: PaddingValues,
val handle: Handle
val handle: Handle,
val map: Map,
) {
@Stable
@Immutable
@ -20,9 +23,21 @@ data class LexiconDimens(
val width: Dp,
val thickness: Dp,
)
@Stable
@Immutable
data class Map(
val mapSnapPx: Int,
val marqueeRadiusPx: Int,
val marqueeStrokePx: Int,
val crossRadiusPx: Int,
val crossStrokePx: Int,
val shadowDropPx: Int,
)
}
fun lexiconDimen(
density: Density,
itemHeight: Dp = 52.dp,
detailPadding: Dp = 248.dp,
itemListPadding: PaddingValues = PaddingValues(
@ -33,9 +48,18 @@ fun lexiconDimen(
width = 32.dp,
thickness = 4.dp,
),
map: LexiconDimens.Map = LexiconDimens.Map(
mapSnapPx = with(density) { 64.dp.roundToPx() },
marqueeRadiusPx = with(density) { 12.dp.roundToPx() },
marqueeStrokePx = with(density) { 2.dp.roundToPx() },
crossRadiusPx = with(density) { 12.dp.roundToPx() / sqrt(2f) }.toInt(),
crossStrokePx = with(density) { 2.dp.roundToPx() },
shadowDropPx = with(density) { 2.dp.roundToPx() },
),
) = LexiconDimens(
item = itemHeight,
detailPadding = detailPadding,
itemListPadding = itemListPadding,
handle = handle,
map = map,
)