Add map feature
This commit is contained in:
parent
34d15c41e0
commit
e74bd7c097
13 changed files with 640 additions and 16 deletions
|
|
@ -22,8 +22,12 @@ class MarqueeParser @Inject constructor() {
|
||||||
item is List<*> -> {
|
item is List<*> -> {
|
||||||
val map = item.getOrNull(structure.map) as? String
|
val map = item.getOrNull(structure.map) as? String
|
||||||
val name = item.getOrNull(structure.name) as? String
|
val name = item.getOrNull(structure.name) as? String
|
||||||
val x = (item.getOrNull(structure.x) as? String)?.toFloatOrNull()
|
val x = (item.getOrNull(structure.x) as? String)
|
||||||
val y = (item.getOrNull(structure.y) as? String)?.toFloatOrNull()
|
?.replace(oldValue = ",", newValue = ".")
|
||||||
|
?.toFloatOrNull()
|
||||||
|
val y = (item.getOrNull(structure.y) as? String)
|
||||||
|
?.replace(oldValue = ",", newValue = ".")
|
||||||
|
?.toFloatOrNull()
|
||||||
val description = item.getOrNull(structure.description) as? String?
|
val description = item.getOrNull(structure.description) as? String?
|
||||||
|
|
||||||
if (map != null && name != null && x != null && y != null) {
|
if (map != null && name != null && x != null && y != null) {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@ import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||||
import com.pixelized.rplexicon.LocalSnack
|
import com.pixelized.rplexicon.LocalSnack
|
||||||
import com.pixelized.rplexicon.R
|
import com.pixelized.rplexicon.R
|
||||||
import com.pixelized.rplexicon.ui.navigation.pages.LEXICON_ROUTE
|
import com.pixelized.rplexicon.ui.navigation.pages.LEXICON_LIST_ROUTE
|
||||||
|
import com.pixelized.rplexicon.ui.navigation.pages.LOCATION_LIST_ROUTE
|
||||||
|
import com.pixelized.rplexicon.ui.navigation.pages.QUEST_LIST_ROUTE
|
||||||
import com.pixelized.rplexicon.ui.navigation.pages.composableLexicon
|
import com.pixelized.rplexicon.ui.navigation.pages.composableLexicon
|
||||||
import com.pixelized.rplexicon.ui.navigation.pages.composableLocations
|
import com.pixelized.rplexicon.ui.navigation.pages.composableLocations
|
||||||
import com.pixelized.rplexicon.ui.navigation.pages.composableQuests
|
import com.pixelized.rplexicon.ui.navigation.pages.composableQuests
|
||||||
|
|
@ -50,7 +52,7 @@ fun HomeNavHost(
|
||||||
lexiconListState: LazyListState,
|
lexiconListState: LazyListState,
|
||||||
questListState: LazyListState,
|
questListState: LazyListState,
|
||||||
locationListState: LazyListState,
|
locationListState: LazyListState,
|
||||||
startDestination: String = LEXICON_ROUTE
|
startDestination: String = LOCATION_LIST_ROUTE
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalSnack provides remember { SnackbarHostState() },
|
LocalSnack provides remember { SnackbarHostState() },
|
||||||
|
|
@ -69,15 +71,17 @@ fun HomeNavHost(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
val selectedIndex = rememberSaveable { mutableStateOf(0) }
|
val selectedIndex = rememberSaveable {
|
||||||
|
mutableStateOf(bottomBarItems.indexOfFirst { it.route == startDestination })
|
||||||
|
}
|
||||||
BottomAppBar(
|
BottomAppBar(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
tonalElevation = 0.dp,
|
tonalElevation = 0.dp,
|
||||||
) {
|
) {
|
||||||
bottomBarItems.forEachIndexed { index, item ->
|
bottomBarItems.forEachIndexed { index, item ->
|
||||||
val selectedItem =
|
val selectedItem = remember {
|
||||||
remember { derivedStateOf { selectedIndex.value == index } }
|
derivedStateOf { selectedIndex.value == index }
|
||||||
|
}
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedItem.value,
|
selected = selectedItem.value,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -118,6 +122,7 @@ fun HomeNavHost(
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class BottomBarItem(
|
class BottomBarItem(
|
||||||
|
val route: String,
|
||||||
val icon: Int,
|
val icon: Int,
|
||||||
val label: Int,
|
val label: Int,
|
||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
|
|
@ -132,16 +137,19 @@ private fun rememberBottomBarItems(
|
||||||
val option = navHostController.pageOption()
|
val option = navHostController.pageOption()
|
||||||
listOf(
|
listOf(
|
||||||
BottomBarItem(
|
BottomBarItem(
|
||||||
|
route = LEXICON_LIST_ROUTE,
|
||||||
icon = R.drawable.ic_outline_account_circle_24,
|
icon = R.drawable.ic_outline_account_circle_24,
|
||||||
label = R.string.home_lexicon,
|
label = R.string.home_lexicon,
|
||||||
onClick = { navHostController.navigateToLexicon(option) }
|
onClick = { navHostController.navigateToLexicon(option) }
|
||||||
),
|
),
|
||||||
BottomBarItem(
|
BottomBarItem(
|
||||||
|
route = QUEST_LIST_ROUTE,
|
||||||
icon = R.drawable.ic_outline_scroll_24,
|
icon = R.drawable.ic_outline_scroll_24,
|
||||||
label = R.string.home_quest_log,
|
label = R.string.home_quest_log,
|
||||||
onClick = { navHostController.navigateToQuestList(option) }
|
onClick = { navHostController.navigateToQuestList(option) }
|
||||||
),
|
),
|
||||||
BottomBarItem(
|
BottomBarItem(
|
||||||
|
route = LOCATION_LIST_ROUTE,
|
||||||
icon = R.drawable.ic_outline_map_24,
|
icon = R.drawable.ic_outline_map_24,
|
||||||
label = R.string.home_location,
|
label = R.string.home_location,
|
||||||
onClick = { navHostController.navigateToLocation(option) }
|
onClick = { navHostController.navigateToLocation(option) }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.pixelized.rplexicon.ui.navigation
|
package com.pixelized.rplexicon.ui.navigation
|
||||||
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
|
@ -16,6 +15,7 @@ import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication
|
||||||
import com.pixelized.rplexicon.ui.navigation.screens.composableHome
|
import com.pixelized.rplexicon.ui.navigation.screens.composableHome
|
||||||
import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconDetail
|
import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconDetail
|
||||||
import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconSearch
|
import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconSearch
|
||||||
|
import com.pixelized.rplexicon.ui.navigation.screens.composableLocationDetail
|
||||||
import com.pixelized.rplexicon.ui.navigation.screens.composableQuestDetail
|
import com.pixelized.rplexicon.ui.navigation.screens.composableQuestDetail
|
||||||
|
|
||||||
val LocalScreenNavHost = staticCompositionLocalOf<NavHostController> {
|
val LocalScreenNavHost = staticCompositionLocalOf<NavHostController> {
|
||||||
|
|
@ -45,11 +45,10 @@ fun ScreenNavHost(
|
||||||
questListState = questListState,
|
questListState = questListState,
|
||||||
locationListState = locationListState
|
locationListState = locationListState
|
||||||
)
|
)
|
||||||
|
|
||||||
composableLexiconDetail()
|
composableLexiconDetail()
|
||||||
composableLexiconSearch()
|
composableLexiconSearch()
|
||||||
|
|
||||||
composableQuestDetail()
|
composableQuestDetail()
|
||||||
|
composableLocationDetail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconScreen
|
||||||
|
|
||||||
private const val ROUTE = "lexicon"
|
private const val ROUTE = "lexicon"
|
||||||
|
|
||||||
const val LEXICON_ROUTE = ROUTE
|
const val LEXICON_LIST_ROUTE = ROUTE
|
||||||
|
|
||||||
fun NavGraphBuilder.composableLexicon(
|
fun NavGraphBuilder.composableLexicon(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
) {
|
) {
|
||||||
animatedComposable(
|
animatedComposable(
|
||||||
route = LEXICON_ROUTE,
|
route = LEXICON_LIST_ROUTE,
|
||||||
animation = NavigationAnimation.NONE,
|
animation = NavigationAnimation.NONE,
|
||||||
) {
|
) {
|
||||||
LexiconScreen(
|
LexiconScreen(
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ import com.pixelized.rplexicon.ui.screens.location.list.LocationScreen
|
||||||
|
|
||||||
private const val ROUTE = "locations"
|
private const val ROUTE = "locations"
|
||||||
|
|
||||||
const val LOCATION_ROUTE = ROUTE
|
const val LOCATION_LIST_ROUTE = ROUTE
|
||||||
|
|
||||||
fun NavGraphBuilder.composableLocations(
|
fun NavGraphBuilder.composableLocations(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
) {
|
) {
|
||||||
animatedComposable(
|
animatedComposable(
|
||||||
route = LOCATION_ROUTE,
|
route = LOCATION_LIST_ROUTE,
|
||||||
animation = NavigationAnimation.NONE,
|
animation = NavigationAnimation.NONE,
|
||||||
) {
|
) {
|
||||||
LocationScreen(
|
LocationScreen(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.pixelized.rplexicon.ui.navigation.screens
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.NavOptionsBuilder
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
|
||||||
|
import com.pixelized.rplexicon.ui.navigation.animatedComposable
|
||||||
|
import com.pixelized.rplexicon.ui.screens.location.detail.LocationDetail
|
||||||
|
import com.pixelized.rplexicon.utilitary.extentions.ARG
|
||||||
|
|
||||||
|
private const val ROUTE = "LocationDetail"
|
||||||
|
private const val ARG_ID = "id"
|
||||||
|
|
||||||
|
val LOCATION_DETAIL_ROUTE = ROUTE +
|
||||||
|
"?${ARG_ID.ARG}"
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
data class LocationDetailArgument(
|
||||||
|
val id: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
val SavedStateHandle.locationDetailArgument: LocationDetailArgument
|
||||||
|
get() = LocationDetailArgument(
|
||||||
|
id = get(ARG_ID) ?: error("CharacterDetailArgument argument: $ARG_ID"),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun NavGraphBuilder.composableLocationDetail() {
|
||||||
|
animatedComposable(
|
||||||
|
route = LOCATION_DETAIL_ROUTE,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(name = ARG_ID) {
|
||||||
|
type = NavType.IntType
|
||||||
|
},
|
||||||
|
),
|
||||||
|
animation = NavigationAnimation.Push,
|
||||||
|
) {
|
||||||
|
LocationDetail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToLocationDetail(
|
||||||
|
id: Int,
|
||||||
|
option: NavOptionsBuilder.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
val route = ROUTE +
|
||||||
|
"?$ARG_ID=$id"
|
||||||
|
|
||||||
|
navigate(route = route, builder = option)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
package com.pixelized.rplexicon.ui.screens.location.detail
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.animateOffsetAsState
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
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.skydoves.landscapist.ImageOptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FantasyMap(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: FantasyMapState,
|
||||||
|
imageOptions: ImageOptions = ImageOptions(),
|
||||||
|
@DrawableRes previewPlaceholder: Int,
|
||||||
|
item: State<LocationDetailUio>,
|
||||||
|
selectedItem: State<Int>,
|
||||||
|
onMarquee: (LocationDetailUio.MarqueeUio) -> Unit,
|
||||||
|
) {
|
||||||
|
val animatedScale = animateFloatAsState(targetValue = state.scale, label = "ScaleAnimation")
|
||||||
|
val animatedOffset = animateOffsetAsState(targetValue = state.offset, label = "OffsetAnimation")
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = "CenterOnMarquee:${selectedItem.value}") {
|
||||||
|
item.value.marquees.getOrNull(selectedItem.value)?.position
|
||||||
|
?.let { state.pan(state.computeMarqueeOffset(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.pointerInput("DetectTransformGestures") {
|
||||||
|
detectTransformGestures(panZoomLock = true) { _, pan, zoom, _ ->
|
||||||
|
state.scale(
|
||||||
|
scale = state.scale * zoom,
|
||||||
|
)
|
||||||
|
state.pan(
|
||||||
|
offset = Offset(
|
||||||
|
x = state.offset.x + pan.x,
|
||||||
|
y = state.offset.y + pan.y,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = animatedScale.value
|
||||||
|
scaleY = animatedScale.value
|
||||||
|
translationX = animatedOffset.value.x
|
||||||
|
translationY = animatedOffset.value.y
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
modifier = Modifier
|
||||||
|
.onSizeChanged { state.imageSize = it }
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
item.value.marquees.forEachIndexed { index, item ->
|
||||||
|
drawCircle(
|
||||||
|
color = when (selectedItem.value) {
|
||||||
|
index -> Color.Cyan
|
||||||
|
else -> Color.White
|
||||||
|
},
|
||||||
|
radius = 12.dp.toPx() / animatedScale.value,
|
||||||
|
style = Stroke(
|
||||||
|
width = 2.dp.toPx() / animatedScale.value
|
||||||
|
),
|
||||||
|
center = Offset(
|
||||||
|
x = size.width * item.position.x,
|
||||||
|
y = size.height * item.position.y
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointerInput("DetectTapGestures") {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = { tap ->
|
||||||
|
val marquee = item.value.marquees
|
||||||
|
.asReversed()
|
||||||
|
.firstOrNull { item ->
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
marquee?.let(onMarquee)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
imageModel = { item.value.map },
|
||||||
|
imageOptions = imageOptions,
|
||||||
|
previewPlaceholder = previewPlaceholder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Stable
|
||||||
|
fun rememberFantasyMapState(
|
||||||
|
initialScale: Float = 1f,
|
||||||
|
initialOffset: Offset = Offset.Zero,
|
||||||
|
minScale: Float = 1f,
|
||||||
|
maxScale: Float = 5f,
|
||||||
|
): FantasyMapState {
|
||||||
|
return remember {
|
||||||
|
FantasyMapState(
|
||||||
|
initialScale = initialScale,
|
||||||
|
initialOffset = initialOffset,
|
||||||
|
minScale = minScale,
|
||||||
|
maxScale = maxScale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class FantasyMapState(
|
||||||
|
initialScale: Float = 1f,
|
||||||
|
initialOffset: Offset = Offset.Zero,
|
||||||
|
val minScale: Float = 1f,
|
||||||
|
val maxScale: Float = 5f,
|
||||||
|
) {
|
||||||
|
private val maxX by derivedStateOf { imageSize.width * scale / 2f }
|
||||||
|
private val minX by derivedStateOf { -maxX }
|
||||||
|
private val maxY by derivedStateOf { imageSize.height * scale / 2f }
|
||||||
|
private val minY by derivedStateOf { -maxY }
|
||||||
|
|
||||||
|
private val _scale: MutableState<Float> = mutableStateOf(initialScale)
|
||||||
|
val scale: Float by _scale
|
||||||
|
|
||||||
|
private val _offset: MutableState<Offset> = mutableStateOf(initialOffset)
|
||||||
|
val offset: Offset by _offset
|
||||||
|
|
||||||
|
private val _imageSize: MutableState<IntSize> = mutableStateOf(IntSize.Zero)
|
||||||
|
var imageSize: IntSize by _imageSize
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
fun scale(
|
||||||
|
scale: Float,
|
||||||
|
) {
|
||||||
|
_scale.value = maxOf(minScale, minOf(scale, maxScale))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
fun pan(
|
||||||
|
offset: Offset,
|
||||||
|
) {
|
||||||
|
_offset.value = Offset(
|
||||||
|
x = maxOf(minX, minOf(maxX, offset.x)),
|
||||||
|
y = maxOf(minY, minOf(maxY, offset.y)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
fun computeMarqueeOffset(
|
||||||
|
origin: Offset,
|
||||||
|
size: IntSize = imageSize,
|
||||||
|
): Offset {
|
||||||
|
return Offset(
|
||||||
|
x = (size.width / 2f - origin.x * size.width) * scale,
|
||||||
|
y = (size.height / 2f - origin.y * size.height) * scale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
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.layout.Box
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledIconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.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.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.pixelized.rplexicon.R
|
||||||
|
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
|
||||||
|
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||||
|
import com.skydoves.landscapist.ImageOptions
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
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
|
||||||
|
fun LocationDetail(
|
||||||
|
viewModel: LocationDetailViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val pager = rememberPagerState()
|
||||||
|
val fantasy = rememberFantasyMapState()
|
||||||
|
val screen = LocalScreenNavHost.current
|
||||||
|
|
||||||
|
val selectedIndex = remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
Surface {
|
||||||
|
LocationContent(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
pagerState = pager,
|
||||||
|
fantasyMapState = fantasy,
|
||||||
|
item = viewModel.location,
|
||||||
|
selectedIndex = selectedIndex,
|
||||||
|
onBack = {
|
||||||
|
screen.popBackStack()
|
||||||
|
},
|
||||||
|
onMarquee = {
|
||||||
|
scope.launch {
|
||||||
|
val index = max(viewModel.location.value.marquees.indexOf(it), 0)
|
||||||
|
selectedIndex.value = index
|
||||||
|
pager.animateScrollToPage(page = index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCenter = {
|
||||||
|
fantasy.scale(scale = 1f)
|
||||||
|
fantasy.pan(offset = Offset.Zero)
|
||||||
|
},
|
||||||
|
onZoomIn = {
|
||||||
|
fantasy.scale(fantasy.scale + 1)
|
||||||
|
fantasy.pan(offset = fantasy.offset)
|
||||||
|
},
|
||||||
|
onZoomOut = {
|
||||||
|
fantasy.scale(fantasy.scale - 1)
|
||||||
|
fantasy.pan(offset = fantasy.offset)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HandlePagerScroll(
|
||||||
|
pagerState = pager,
|
||||||
|
index = selectedIndex,
|
||||||
|
onIndexChange = { page ->
|
||||||
|
pager.animateScrollToPage(page = page)
|
||||||
|
},
|
||||||
|
onPageChange = { page ->
|
||||||
|
selectedIndex.value = page
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun LocationContent(
|
||||||
|
modifier: Modifier,
|
||||||
|
pagerState: PagerState,
|
||||||
|
fantasyMapState: FantasyMapState,
|
||||||
|
item: State<LocationDetailUio>,
|
||||||
|
selectedIndex: State<Int>,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onMarquee: (LocationDetailUio.MarqueeUio) -> Unit,
|
||||||
|
onCenter: () -> Unit,
|
||||||
|
onZoomIn: () -> Unit,
|
||||||
|
onZoomOut: () -> Unit,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_arrow_back_ios_new_24),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(id = R.string.detail_title))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(paddingValues = paddingValues),
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(weight = 2f),
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(shape = RectangleShape),
|
||||||
|
) {
|
||||||
|
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),
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(
|
||||||
|
pagerState: PagerState,
|
||||||
|
index: State<Int>,
|
||||||
|
onIndexChange: suspend (page: Int) -> Unit,
|
||||||
|
onPageChange: suspend (page: Int) -> Unit,
|
||||||
|
) {
|
||||||
|
if (!pagerState.isScrollInProgress) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
onPageChange.invoke(pagerState.currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(key1 = index.value) {
|
||||||
|
onIndexChange.invoke(index.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
private fun LocationPreview() {
|
||||||
|
LexiconTheme {
|
||||||
|
Surface {
|
||||||
|
LocationContent(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
pagerState = rememberPagerState(),
|
||||||
|
fantasyMapState = rememberFantasyMapState(),
|
||||||
|
item = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
LocationDetailUio(
|
||||||
|
name = "Daggerfall",
|
||||||
|
map = Uri.parse("https://i.pinimg.com/originals/6d/56/cd/6d56cd9358cc94a7077157ea3c1b5842.jpg"),
|
||||||
|
marquees = listOf(
|
||||||
|
LocationDetailUio.MarqueeUio(
|
||||||
|
name = "start",
|
||||||
|
position = Offset.Zero,
|
||||||
|
description = "Marquee en haut à gauche."
|
||||||
|
),
|
||||||
|
LocationDetailUio.MarqueeUio(
|
||||||
|
name = "end",
|
||||||
|
position = Offset(1f, 1f),
|
||||||
|
description = "Marquee en bas à droite."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selectedIndex = remember { mutableStateOf(0) },
|
||||||
|
onBack = { },
|
||||||
|
onMarquee = { },
|
||||||
|
onCenter = { },
|
||||||
|
onZoomIn = { },
|
||||||
|
onZoomOut = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
||||||
|
import com.pixelized.rplexicon.ui.navigation.screens.locationDetailArgument
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class LocationDetailViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
repository: LocationRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
val location: State<LocationDetailUio>
|
||||||
|
|
||||||
|
init {
|
||||||
|
val argument = savedStateHandle.locationDetailArgument
|
||||||
|
val source = repository.data.value[argument.id]
|
||||||
|
|
||||||
|
location = mutableStateOf(
|
||||||
|
LocationDetailUio(
|
||||||
|
name = source.name,
|
||||||
|
map = source.uri,
|
||||||
|
marquees = source.marquees.map { marquee ->
|
||||||
|
LocationDetailUio.MarqueeUio(
|
||||||
|
name = marquee.name,
|
||||||
|
position = Offset(x = marquee.x, y = marquee.y),
|
||||||
|
description = marquee.description,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.pixelized.rplexicon.ui.composable.Loader
|
import com.pixelized.rplexicon.ui.composable.Loader
|
||||||
import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
|
import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
|
||||||
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
|
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
|
||||||
|
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocationDetail
|
||||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||||
import com.pixelized.rplexicon.utilitary.extentions.cell
|
import com.pixelized.rplexicon.utilitary.extentions.cell
|
||||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
|
import com.pixelized.rplexicon.utilitary.extentions.lexicon
|
||||||
|
|
@ -59,7 +60,7 @@ fun LocationScreen(
|
||||||
refreshState = refresh,
|
refreshState = refresh,
|
||||||
refreshing = viewModel.isLoading,
|
refreshing = viewModel.isLoading,
|
||||||
onItem = {
|
onItem = {
|
||||||
// screen.navigateToQuestDetail(id = it.id)
|
screen.navigateToLocationDetail(id = it.id)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
5
app/src/main/res/drawable/ic_baseline_add_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_add_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_baseline_remove_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_remove_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19,13H5v-2h14v2z"/>
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_baseline_zoom_in_map_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_zoom_in_map_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M9,9l0,-6L7,3l0,2.59L3.91,2.5L2.5,3.91L5.59,7L3,7l0,2L9,9zM21,9V7l-2.59,0l3.09,-3.09L20.09,2.5L17,5.59V3l-2,0l0,6L21,9zM3,15l0,2h2.59L2.5,20.09l1.41,1.41L7,18.41L7,21h2l0,-6L3,15zM15,15l0,6h2v-2.59l3.09,3.09l1.41,-1.41L18.41,17H21v-2L15,15z"/>
|
||||||
|
</vector>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue