Change the map detail design
This commit is contained in:
parent
e74bd7c097
commit
d09b16ee33
9 changed files with 279 additions and 119 deletions
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue