Change the map detail design

This commit is contained in:
Thomas Andres Gomez 2023-08-09 12:29:19 +02:00
parent e74bd7c097
commit d09b16ee33
9 changed files with 279 additions and 119 deletions

View file

@ -0,0 +1,29 @@
package com.pixelized.rplexicon.ui.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Composable
fun Handle(
modifier: Modifier = Modifier,
width: Dp = MaterialTheme.lexicon.dimens.handle.width,
thickness: Dp = MaterialTheme.lexicon.dimens.handle.thickness,
color: Color = MaterialTheme.lexicon.colorScheme.handle,
shape: Shape = CircleShape
) = Box(
modifier = modifier
.size(width = width, height = thickness)
.background(
color = color,
shape = shape,
)
)

View file

@ -38,7 +38,7 @@ fun FantasyMap(
@DrawableRes previewPlaceholder: Int,
item: State<LocationDetailUio>,
selectedItem: State<Int>,
onMarquee: (LocationDetailUio.MarqueeUio) -> Unit,
onMarquee: (MarqueeUio) -> Unit,
) {
val animatedScale = animateFloatAsState(targetValue = state.scale, label = "ScaleAnimation")
val animatedOffset = animateOffsetAsState(targetValue = state.offset, label = "OffsetAnimation")
@ -52,14 +52,19 @@ fun FantasyMap(
modifier = modifier
.pointerInput("DetectTransformGestures") {
detectTransformGestures(panZoomLock = true) { _, pan, zoom, _ ->
val newScale = state.scale * zoom
val oldScale = state.scale
val oldOffset = state.offset
state.scale(
scale = state.scale * zoom,
scale = newScale
)
state.pan(
offset = Offset(
x = state.offset.x + pan.x,
y = state.offset.y + pan.y,
),
x = oldOffset.x * newScale / oldScale + pan.x,
y = oldOffset.y * newScale / oldScale + pan.y,
)
)
}
}
@ -99,7 +104,7 @@ fun FantasyMap(
val marquee = item.value.marquees
.asReversed()
.firstOrNull { item ->
val radius = 24.dp.toPx() * animatedScale.value
val radius = 24.dp.toPx() / animatedScale.value
(size.width * item.position.x).let { tap.x in (it - radius)..(it + radius) } &&
(size.height * item.position.y).let { tap.y in (it - radius)..(it + radius) }
}

View file

@ -3,16 +3,23 @@ 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.layout.Arrangement
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
@ -34,21 +41,24 @@ 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.Color
import androidx.compose.ui.graphics.RectangleShape
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.res.painterResource
import androidx.compose.ui.res.stringResource
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.IntOffset
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.skydoves.landscapist.ImageOptions
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.math.max
@Stable
@ -56,14 +66,7 @@ data class LocationDetailUio(
val name: String,
val map: Uri,
val marquees: List<MarqueeUio>,
) {
@Stable
data class MarqueeUio(
val name: String,
val position: Offset,
val description: String?,
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -71,15 +74,35 @@ fun LocationDetail(
viewModel: LocationDetailViewModel = hiltViewModel()
) {
val scope = rememberCoroutineScope()
val scroll = rememberScrollState()
val pager = rememberPagerState()
val fantasy = rememberFantasyMapState()
val screen = LocalScreenNavHost.current
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 selectedIndex = remember { mutableStateOf(0) }
Surface {
LocationContent(
modifier = Modifier.fillMaxSize(),
connection = connection,
scrollState = scroll,
pagerState = pager,
fantasyMapState = fantasy,
item = viewModel.location,
@ -99,12 +122,32 @@ fun LocationDetail(
fantasy.pan(offset = Offset.Zero)
},
onZoomIn = {
fantasy.scale(fantasy.scale + 1)
fantasy.pan(offset = fantasy.offset)
val newScale = fantasy.scale + 1
if (newScale <= fantasy.maxScale) {
val oldScale = fantasy.scale
val oldOffset = fantasy.offset
fantasy.scale(newScale)
fantasy.pan(
offset = Offset(
x = oldOffset.x * newScale / oldScale,
y = oldOffset.y * newScale / oldScale,
)
)
}
},
onZoomOut = {
fantasy.scale(fantasy.scale - 1)
fantasy.pan(offset = fantasy.offset)
val newScale = fantasy.scale - 1
if (newScale >= fantasy.minScale) {
val oldScale = fantasy.scale
val oldOffset = fantasy.offset
fantasy.scale(newScale)
fantasy.pan(
offset = Offset(
x = oldOffset.x * newScale / oldScale,
y = oldOffset.y * newScale / oldScale,
)
)
}
},
)
@ -125,19 +168,20 @@ fun LocationDetail(
@Composable
private fun LocationContent(
modifier: Modifier,
connection: NestedScrollConnection,
scrollState: ScrollState,
pagerState: PagerState,
fantasyMapState: FantasyMapState,
item: State<LocationDetailUio>,
selectedIndex: State<Int>,
onBack: () -> Unit,
onMarquee: (LocationDetailUio.MarqueeUio) -> Unit,
onMarquee: (MarqueeUio) -> Unit,
onCenter: () -> Unit,
onZoomIn: () -> Unit,
onZoomOut: () -> Unit,
) {
Scaffold(
modifier = modifier,
containerColor = Color.Transparent,
topBar = {
TopAppBar(
navigationIcon = {
@ -149,27 +193,33 @@ private fun LocationContent(
}
},
title = {
Text(text = stringResource(id = R.string.detail_title))
Text(text = stringResource(id = R.string.map_title))
},
)
},
) { paddingValues ->
Column(
BoxWithConstraints(
modifier = Modifier.padding(paddingValues = paddingValues),
) constraint@{
Column(
modifier = Modifier.verticalScroll(state = scrollState),
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.weight(weight = 2f),
.heightIn(
min = this@constraint.maxHeight / 2,
max = this@constraint.maxHeight * 2 / 3,
),
tonalElevation = 2.dp,
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(shape = RectangleShape),
modifier = Modifier.clip(shape = RectangleShape),
) {
FantasyMap(
modifier = Modifier.align(alignment = Alignment.Center),
modifier = Modifier
.align(alignment = Alignment.Center)
.offset { IntOffset(x = 0, y = scrollState.value / 2) },
state = fantasyMapState,
previewPlaceholder = R.drawable.im_brulkhai,
imageOptions = ImageOptions(contentScale = ContentScale.Fit),
@ -216,16 +266,28 @@ private fun LocationContent(
}
}
}
Handle(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(top = 16.dp)
)
HorizontalPager(
modifier = Modifier.weight(weight = 1f),
modifier = Modifier
.fillMaxWidth()
.nestedScroll(connection),
state = pagerState,
verticalAlignment = Alignment.Top,
pageCount = item.value.marquees.size,
contentPadding = PaddingValues(all = 16.dp),
pageSpacing = 8.dp,
pageSpacing = 16.dp,
) {
item.value.marquees.getOrNull(it)?.let { marquee ->
Marquee(
modifier = Modifier.fillMaxSize(),
MarqueeItem(
modifier = Modifier
.fillMaxWidth()
.height(this@constraint.maxHeight - 32.dp),
marquee = marquee,
)
}
@ -233,31 +295,6 @@ private fun LocationContent(
}
}
}
@Composable
private fun Marquee(
modifier: Modifier = Modifier,
marquee: LocationDetailUio.MarqueeUio,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.headlineSmall,
text = marquee.name,
)
marquee.description?.let {
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@ -287,6 +324,8 @@ private fun LocationPreview() {
Surface {
LocationContent(
modifier = Modifier.fillMaxSize(),
connection = remember { object : NestedScrollConnection {} },
scrollState = rememberScrollState(),
pagerState = rememberPagerState(),
fantasyMapState = rememberFantasyMapState(),
item = remember {
@ -295,12 +334,12 @@ private fun LocationPreview() {
name = "Daggerfall",
map = Uri.parse("https://i.pinimg.com/originals/6d/56/cd/6d56cd9358cc94a7077157ea3c1b5842.jpg"),
marquees = listOf(
LocationDetailUio.MarqueeUio(
MarqueeUio(
name = "start",
position = Offset.Zero,
description = "Marquee en haut à gauche."
),
LocationDetailUio.MarqueeUio(
MarqueeUio(
name = "end",
position = Offset(1f, 1f),
description = "Marquee en bas à droite."

View file

@ -26,7 +26,7 @@ class LocationDetailViewModel @Inject constructor(
name = source.name,
map = source.uri,
marquees = source.marquees.map { marquee ->
LocationDetailUio.MarqueeUio(
MarqueeUio(
name = marquee.name,
position = Offset(x = marquee.x, y = marquee.y),
description = marquee.description,

View file

@ -0,0 +1,68 @@
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.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
@Stable
data class MarqueeUio(
val name: String,
val position: Offset,
val description: String?,
)
@Composable
fun MarqueeItem(
modifier: Modifier = Modifier,
marquee: MarqueeUio,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.headlineSmall,
text = marquee.name,
)
marquee.description?.let {
Text(
modifier = Modifier.verticalScroll(rememberScrollState()),
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
}
}
@Composable
@Preview
private fun MarqueeItemPreview() {
LexiconTheme {
MarqueeItem(
modifier = Modifier.fillMaxSize(),
marquee = MarqueeUio(
name = "Name",
position = Offset.Zero,
description = "description",
)
)
}
}

View file

@ -14,6 +14,7 @@ class LexiconColors(
val status: Color,
val navigation: Color,
val placeholder: Color,
val handle: Color,
)
@Stable
@ -32,6 +33,7 @@ fun darkColorScheme(
status = status,
navigation = navigation,
placeholder = placeholder,
handle = base.onSurface.copy(alpha = 0.5f),
)
@Stable
@ -50,4 +52,5 @@ fun lightColorScheme(
status = status,
navigation = navigation,
placeholder = placeholder,
handle = base.onSurface.copy(alpha = 0.5f),
)

View file

@ -12,7 +12,15 @@ data class LexiconDimens(
val item: Dp,
val detailPadding: Dp,
val itemListPadding: PaddingValues,
val handle: Handle
) {
@Stable
@Immutable
data class Handle(
val width: Dp,
val thickness: Dp,
)
}
fun lexiconDimen(
itemHeight: Dp = 52.dp,
@ -21,8 +29,13 @@ fun lexiconDimen(
top = 8.dp,
bottom = 8.dp + 16.dp + 56.dp + 16.dp,
),
handle: LexiconDimens.Handle = LexiconDimens.Handle(
width = 32.dp,
thickness = 4.dp,
),
) = LexiconDimens(
item = itemHeight,
detailPadding = detailPadding,
itemListPadding = itemListPadding
itemListPadding = itemListPadding,
handle = handle,
)

View file

@ -52,4 +52,6 @@
<string name="quest_detail_area">Lieu :</string>
<string name="quest_detail_individual_reward">Récompense individuelle :</string>
<string name="quest_detail_group_rewars">Récompense de groupe :</string>
<string name="map_title">Carte</string>
</resources>

View file

@ -53,4 +53,5 @@
<string name="quest_detail_individual_reward">Individual reward:</string>
<string name="quest_detail_group_rewars">Group reward:</string>
<string name="map_title">Map</string>
</resources>