Styling up the LocationDetail

This commit is contained in:
Thomas Andres Gomez 2023-08-11 15:06:36 +02:00
parent e3cd0bdd4b
commit b9d14d12ff
11 changed files with 156 additions and 55 deletions

View file

@ -100,7 +100,7 @@ fun LexiconItem(
true -> emptyList()
else -> listOf(
AnnotatedString.Range(
item = typography.dropCapMediumSpan,
item = typography.bodyDropCapSpan,
start = 0,
end = 1,
)

View file

@ -31,7 +31,6 @@ 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
@ -39,12 +38,13 @@ import com.skydoves.landscapist.ImageOptions
fun FantasyMap(
modifier: Modifier = Modifier,
state: FantasyMapState,
model: () -> Any?,
imageOptions: ImageOptions = ImageOptions(),
@DrawableRes previewPlaceholder: Int,
item: State<LocationDetailUio>,
items: State<List<AnnotatedMarqueeUio>>,
highlight: State<Offset>,
selectedItem: State<Int>,
onMarquee: (MarqueeUio) -> Unit,
onMarquee: (AnnotatedMarqueeUio) -> Unit,
onTap: (Offset) -> Unit,
) {
val lexiconTheme = MaterialTheme.lexicon
@ -68,7 +68,7 @@ fun FantasyMap(
}
LaunchedEffect(key1 = "CenterOnMarquee:${selectedItem.value}") {
item.value.marquees.getOrNull(selectedItem.value)?.position
items.value.getOrNull(selectedItem.value)?.position
?.let { state.pan(state.computeMarqueeOffset(it)) }
}
@ -115,7 +115,7 @@ fun FantasyMap(
.drawWithContent {
drawContent()
if (animatedMarqueeAlpha.value > 0f) {
item.value.marquees.forEachIndexed { index, item ->
items.value.forEachIndexed { index, item ->
if (item.position != Offset.Unspecified) {
drawMarque(
theme = lexiconTheme,
@ -147,7 +147,7 @@ fun FantasyMap(
)
)
} else {
val marquee = item.value.marquees
val marquee = items.value
.asReversed()
.firstOrNull { item ->
if (item.position != Offset.Unspecified) {
@ -163,7 +163,7 @@ fun FantasyMap(
}
)
},
imageModel = { item.value.map },
imageModel = model,
imageOptions = imageOptions,
previewPlaceholder = previewPlaceholder,
)

View file

@ -3,6 +3,7 @@ package com.pixelized.rplexicon.ui.screens.location.detail
import android.content.res.Configuration
import android.net.Uri
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
@ -36,6 +37,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -43,13 +45,18 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
@ -62,7 +69,6 @@ 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
@ -77,6 +83,13 @@ data class LocationDetailUio(
val marquees: List<MarqueeUio>,
)
@Stable
data class AnnotatedLocationDetailUio(
val name: AnnotatedString,
val map: Uri,
val marquees: List<AnnotatedMarqueeUio>,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LocationDetail(
@ -94,6 +107,7 @@ fun LocationDetail(
val ok = stringResource(id = android.R.string.ok)
val location = rememberAnnotation(item = viewModel.location)
val snackJob = remember { mutableStateOf<Job?>(null) }
val mapHighlight = remember { mutableStateOf(Offset.Unspecified) }
val selectedIndex = remember { mutableStateOf(0) }
@ -107,7 +121,7 @@ fun LocationDetail(
scrollState = scroll,
pagerState = pager,
fantasyMapState = fantasy,
item = viewModel.location,
item = location,
selectedIndex = selectedIndex,
mapHighlight = mapHighlight,
onBack = {
@ -115,7 +129,7 @@ fun LocationDetail(
},
onMarquee = {
scope.launch {
val index = max(viewModel.location.value.marquees.indexOf(it), 0)
val index = max(location.value.marquees.indexOf(it), 0)
selectedIndex.value = index
pager.animateScrollToPage(page = index)
}
@ -195,18 +209,19 @@ private fun LocationContent(
scrollState: ScrollState,
pagerState: PagerState,
fantasyMapState: FantasyMapState,
item: State<LocationDetailUio>,
item: State<AnnotatedLocationDetailUio>,
selectedIndex: State<Int>,
mapHighlight: State<Offset>,
onBack: () -> Unit,
onMarquee: (MarqueeUio) -> Unit,
onMarquee: (AnnotatedMarqueeUio) -> Unit,
onMapTap: (Offset) -> Unit,
onTouch: (Boolean) -> Unit,
onCenter: () -> Unit,
onZoomIn: () -> Unit,
onZoomOut: () -> Unit,
) {
val itemNameSize = rememberTextSize(style = MaterialTheme.typography.headlineSmall)
val density = LocalDensity.current
val itemNameSize = remember { mutableStateOf(0.dp) }
val filledIconButtonColors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.surface,
@ -261,9 +276,10 @@ private fun LocationContent(
.align(alignment = Alignment.Center)
.offset { IntOffset(x = 0, y = scrollState.value / 2) },
state = fantasyMapState,
model = { item.value.map },
previewPlaceholder = R.drawable.im_brulkhai,
imageOptions = ImageOptions(contentScale = ContentScale.Fit),
item = item,
items = remember { derivedStateOf { item.value.marquees } },
selectedItem = selectedIndex,
highlight = mapHighlight,
onMarquee = onMarquee,
@ -323,18 +339,32 @@ private fun LocationContent(
Handle(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(top = 16.dp)
.padding(vertical = 16.dp)
)
Text(
Column(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.onSizeChanged { itemNameSize.value = with(density) { it.height.toDp() } }
.padding(horizontal = 16.dp)
.padding(top = 16.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
text = item.value.name,
)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
text = item.value.name,
)
Image(
modifier = Modifier
.height(24.dp)
.graphicsLayer { rotationZ = 180f },
painter = painterResource(id = R.drawable.art_divider_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
HorizontalPager(
modifier = Modifier
@ -350,7 +380,7 @@ private fun LocationContent(
MarqueeItem(
modifier = Modifier
.fillMaxWidth()
.height(this@constraint.maxHeight - 32.dp - itemNameSize.height),
.height(this@constraint.maxHeight - 32.dp - itemNameSize.value),
marquee = marquee,
)
}
@ -378,6 +408,31 @@ private fun HandlePagerScroll(
}
}
@Composable
@Stable
fun rememberAnnotation(item: State<LocationDetailUio>): State<AnnotatedLocationDetailUio> {
val typography = MaterialTheme.lexicon.typography
return remember(item) {
derivedStateOf {
AnnotatedLocationDetailUio(
name = AnnotatedString(
text = item.value.name,
spanStyles = listOf(
AnnotatedString.Range(
item = typography.titleDropCapSpan,
start = 0,
end = Integer.min(1, item.value.name.length),
)
)
),
map = item.value.map,
marquees = item.value.marquees.map { it.annotate(typography) },
)
}
}
}
@Composable
@Stable
private fun rememberSnapConnection(
@ -437,7 +492,7 @@ private fun LocationPreview() {
scrollState = rememberScrollState(),
pagerState = rememberPagerState(),
fantasyMapState = rememberFantasyMapState(),
item = remember {
item = rememberAnnotation(item = remember {
mutableStateOf(
LocationDetailUio(
name = "Daggerfall",
@ -456,7 +511,7 @@ private fun LocationPreview() {
),
)
)
},
}),
selectedIndex = remember { mutableStateOf(0) },
mapHighlight = remember { mutableStateOf(Offset(0.5f, 0.5f)) },
onBack = { },

View file

@ -2,7 +2,6 @@ package com.pixelized.rplexicon.ui.screens.location.detail
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Offset
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.repository.LocationRepository

View file

@ -2,8 +2,7 @@ package com.pixelized.rplexicon.ui.screens.location.detail
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -14,11 +13,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.AnnotatedString
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.unit.dp
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.typography.LexiconTypography
import com.pixelized.rplexicon.utilitary.LOS_HOLLOW
import com.pixelized.rplexicon.utilitary.extentions.annotateWithDropCap
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable
data class MarqueeUio(
@ -27,23 +31,49 @@ data class MarqueeUio(
val description: String?,
)
@Stable
data class AnnotatedMarqueeUio(
val name: AnnotatedString,
val position: Offset,
val description: AnnotatedString?,
)
@Stable
fun MarqueeUio.annotate(
typography: LexiconTypography
): AnnotatedMarqueeUio {
return AnnotatedMarqueeUio(
name = name.annotateWithDropCap(style = typography.bodyDropCapSpan),
position = position,
description = description?.annotateWithDropCap(style = typography.bodyDropCapSpan),
)
}
@Composable
fun MarqueeItem(
modifier: Modifier = Modifier,
marquee: MarqueeUio,
marquee: AnnotatedMarqueeUio,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.titleMedium,
text = marquee.name,
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
text = LOS_HOLLOW,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 3,
text = marquee.name,
)
}
marquee.description?.let {
Text(
modifier = Modifier.verticalScroll(rememberScrollState()),
@ -65,6 +95,8 @@ private fun MarqueeItemPreview() {
name = "Name",
position = Offset.Zero,
description = "description",
).annotate(
typography = MaterialTheme.lexicon.typography,
)
)
}

View file

@ -77,7 +77,7 @@ fun LocationItem(
true -> emptyList()
else -> listOf(
AnnotatedString.Range(
item = typography.dropCapMediumSpan,
item = typography.bodyDropCapSpan,
start = 0,
end = 1,
)

View file

@ -34,7 +34,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
@ -129,7 +128,7 @@ private fun QuestDetailUio.QuestStep.annotate(): AnnotatedQuestDetailUio.Annotat
text = description,
spanStyles = listOf(
AnnotatedString.Range(
item = typography.dropCapLargeSpan,
item = typography.bodyDropCapSpan,
start = 0,
end = min(1, description.length),
)

View file

@ -81,7 +81,7 @@ fun QuestItem(
true -> emptyList()
else -> listOf(
AnnotatedString.Range(
item = typography.dropCapMediumSpan,
item = typography.bodyDropCapSpan,
start = 0,
end = 1,
)

View file

@ -49,7 +49,7 @@ fun lexiconDimen(
thickness = 4.dp,
),
map: LexiconDimens.Map = LexiconDimens.Map(
mapSnapPx = with(density) { 64.dp.roundToPx() },
mapSnapPx = with(density) { 128.dp.roundToPx() },
marqueeRadiusPx = with(density) { 12.dp.roundToPx() },
marqueeStrokePx = with(density) { 2.dp.roundToPx() },
crossRadiusPx = with(density) { 12.dp.roundToPx() / sqrt(2f) }.toInt(),

View file

@ -24,22 +24,19 @@ val stampFontFamily = FontFamily(
@Stable
class LexiconTypography(
val base: Typography = Typography(),
val dropCapMedium: TextStyle = base.displaySmall.copy(
val stamp: TextStyle = base.headlineLarge.copy(
fontFamily = stampFontFamily,
),
val bodyDropCapSpan: SpanStyle = base.displaySmall.copy(
fontFamily = regalFontFamily,
baselineShift = BaselineShift(-0.3f),
letterSpacing = (-6).sp
),
val dropCapLarge: TextStyle = base.displayMedium.copy(
).toSpanStyle(),
val titleDropCapSpan: SpanStyle = base.displayLarge.copy(
fontFamily = regalFontFamily,
baselineShift = BaselineShift.Subscript,
baselineShift = BaselineShift(-0.3f),
letterSpacing = (-8).sp
),
val stamp: TextStyle = base.headlineLarge.copy(
fontFamily = stampFontFamily,
)
) {
val dropCapMediumSpan: SpanStyle = dropCapMedium.toSpanStyle()
val dropCapLargeSpan: SpanStyle = dropCapLarge.toSpanStyle()
}
).toSpanStyle()
)
fun lexiconTypography() = LexiconTypography()

View file

@ -1,6 +1,10 @@
package com.pixelized.rplexicon.utilitary.extentions
import android.net.Uri
import android.view.accessibility.AccessibilityNodeInfo
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.core.net.toUri
val String.ARG: String get() = "$this={$this}"
@ -24,3 +28,18 @@ fun String?.toUriOrNull(): Uri? = try {
null
}
private val dropCapRegex = Regex("(?:\n\n)(.)")
@Stable
fun String.annotateWithDropCap(
style: SpanStyle,
) = AnnotatedString(
text = this,
spanStyles = listOf(
AnnotatedString.Range(
item = style,
start = 0,
end = Integer.min(1, this.length),
),
) + dropCapRegex.annotatedSpan(input = this, spanStyle = style)
)