Add snaping beavior to the LocationDetail
This commit is contained in:
parent
85cdd69570
commit
e3cd0bdd4b
4 changed files with 133 additions and 72 deletions
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue