diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/Handle.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/Handle.kt new file mode 100644 index 0000000..29bac12 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/Handle.kt @@ -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, + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/FantasyMap.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/FantasyMap.kt index c9557bb..67b795c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/FantasyMap.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/FantasyMap.kt @@ -38,7 +38,7 @@ fun FantasyMap( @DrawableRes previewPlaceholder: Int, item: State, selectedItem: State, - 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) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt index c1165bf..74c34b1 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt @@ -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, -) { - @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, selectedIndex: State, 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,117 +193,110 @@ 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), - ) { - Surface( - modifier = Modifier - .fillMaxWidth() - .weight(weight = 2f), - tonalElevation = 2.dp, + ) constraint@{ + Column( + modifier = Modifier.verticalScroll(state = scrollState), ) { - Box( + Surface( modifier = Modifier - .fillMaxSize() - .clip(shape = RectangleShape), + .fillMaxWidth() + .heightIn( + min = this@constraint.maxHeight / 2, + max = this@constraint.maxHeight * 2 / 3, + ), + tonalElevation = 2.dp, ) { - FantasyMap( - modifier = Modifier.align(alignment = Alignment.Center), - state = fantasyMapState, - previewPlaceholder = R.drawable.im_brulkhai, - imageOptions = ImageOptions(contentScale = ContentScale.Fit), - item = item, - selectedItem = selectedIndex, - onMarquee = onMarquee, - ) - Column( - modifier = Modifier - .align(alignment = Alignment.BottomEnd) - .padding(all = 16.dp), + Box( + modifier = Modifier.clip(shape = RectangleShape), ) { - val colors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, + FantasyMap( + 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), + item = item, + selectedItem = selectedIndex, + onMarquee = onMarquee, ) - FilledIconButton( - onClick = onZoomOut, - colors = colors, + Column( + modifier = Modifier + .align(alignment = Alignment.BottomEnd) + .padding(all = 16.dp), ) { - Icon( - painter = painterResource(id = R.drawable.ic_baseline_remove_24), - contentDescription = null - ) - } - FilledIconButton( - onClick = onZoomIn, - colors = colors, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_baseline_add_24), - contentDescription = null - ) - } - FilledIconButton( - onClick = onCenter, - colors = colors, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_baseline_zoom_in_map_24), - contentDescription = null + val colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, ) + FilledIconButton( + onClick = onZoomOut, + colors = colors, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_remove_24), + contentDescription = null + ) + } + FilledIconButton( + onClick = onZoomIn, + colors = colors, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_add_24), + contentDescription = null + ) + } + FilledIconButton( + onClick = onCenter, + colors = colors, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_zoom_in_map_24), + contentDescription = null + ) + } } } } - } - HorizontalPager( - modifier = Modifier.weight(weight = 1f), - state = pagerState, - pageCount = item.value.marquees.size, - contentPadding = PaddingValues(all = 16.dp), - pageSpacing = 8.dp, - ) { - item.value.marquees.getOrNull(it)?.let { marquee -> - Marquee( - modifier = Modifier.fillMaxSize(), - marquee = marquee, - ) + + Handle( + modifier = Modifier + .align(alignment = Alignment.CenterHorizontally) + .padding(top = 16.dp) + ) + + HorizontalPager( + modifier = Modifier + .fillMaxWidth() + .nestedScroll(connection), + state = pagerState, + verticalAlignment = Alignment.Top, + pageCount = item.value.marquees.size, + contentPadding = PaddingValues(all = 16.dp), + pageSpacing = 16.dp, + ) { + item.value.marquees.getOrNull(it)?.let { marquee -> + MarqueeItem( + modifier = Modifier + .fillMaxWidth() + .height(this@constraint.maxHeight - 32.dp), + marquee = marquee, + ) + } } } } } } -@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) @Composable private fun HandlePagerScroll( @@ -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." diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt index a45cffd..ffb0d8f 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt @@ -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, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/MarqueeItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/MarqueeItem.kt new file mode 100644 index 0000000..9f2a6d3 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/MarqueeItem.kt @@ -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", + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt index 4f37718..af27a6c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt @@ -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), ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt index 5faf01e..daa1f3b 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt @@ -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, ) \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3d1f632..4b1a396 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -52,4 +52,6 @@ Lieu : Récompense individuelle : Récompense de groupe : + + Carte \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db1eb4c..bf15819 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,4 +53,5 @@ Individual reward: Group reward: + Map \ No newline at end of file