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<*> -> {
|
||||
val map = item.getOrNull(structure.map) as? String
|
||||
val name = item.getOrNull(structure.name) as? String
|
||||
val x = (item.getOrNull(structure.x) as? String)?.toFloatOrNull()
|
||||
val y = (item.getOrNull(structure.y) as? String)?.toFloatOrNull()
|
||||
val x = (item.getOrNull(structure.x) as? String)
|
||||
?.replace(oldValue = ",", newValue = ".")
|
||||
?.toFloatOrNull()
|
||||
val y = (item.getOrNull(structure.y) as? String)
|
||||
?.replace(oldValue = ",", newValue = ".")
|
||||
?.toFloatOrNull()
|
||||
val description = item.getOrNull(structure.description) as? String?
|
||||
|
||||
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.pixelized.rplexicon.LocalSnack
|
||||
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.composableLocations
|
||||
import com.pixelized.rplexicon.ui.navigation.pages.composableQuests
|
||||
|
|
@ -50,7 +52,7 @@ fun HomeNavHost(
|
|||
lexiconListState: LazyListState,
|
||||
questListState: LazyListState,
|
||||
locationListState: LazyListState,
|
||||
startDestination: String = LEXICON_ROUTE
|
||||
startDestination: String = LOCATION_LIST_ROUTE
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalSnack provides remember { SnackbarHostState() },
|
||||
|
|
@ -69,15 +71,17 @@ fun HomeNavHost(
|
|||
)
|
||||
},
|
||||
bottomBar = {
|
||||
val selectedIndex = rememberSaveable { mutableStateOf(0) }
|
||||
|
||||
val selectedIndex = rememberSaveable {
|
||||
mutableStateOf(bottomBarItems.indexOfFirst { it.route == startDestination })
|
||||
}
|
||||
BottomAppBar(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 0.dp,
|
||||
) {
|
||||
bottomBarItems.forEachIndexed { index, item ->
|
||||
val selectedItem =
|
||||
remember { derivedStateOf { selectedIndex.value == index } }
|
||||
val selectedItem = remember {
|
||||
derivedStateOf { selectedIndex.value == index }
|
||||
}
|
||||
NavigationBarItem(
|
||||
selected = selectedItem.value,
|
||||
onClick = {
|
||||
|
|
@ -118,6 +122,7 @@ fun HomeNavHost(
|
|||
|
||||
@Stable
|
||||
class BottomBarItem(
|
||||
val route: String,
|
||||
val icon: Int,
|
||||
val label: Int,
|
||||
val onClick: () -> Unit,
|
||||
|
|
@ -132,16 +137,19 @@ private fun rememberBottomBarItems(
|
|||
val option = navHostController.pageOption()
|
||||
listOf(
|
||||
BottomBarItem(
|
||||
route = LEXICON_LIST_ROUTE,
|
||||
icon = R.drawable.ic_outline_account_circle_24,
|
||||
label = R.string.home_lexicon,
|
||||
onClick = { navHostController.navigateToLexicon(option) }
|
||||
),
|
||||
BottomBarItem(
|
||||
route = QUEST_LIST_ROUTE,
|
||||
icon = R.drawable.ic_outline_scroll_24,
|
||||
label = R.string.home_quest_log,
|
||||
onClick = { navHostController.navigateToQuestList(option) }
|
||||
),
|
||||
BottomBarItem(
|
||||
route = LOCATION_LIST_ROUTE,
|
||||
icon = R.drawable.ic_outline_map_24,
|
||||
label = R.string.home_location,
|
||||
onClick = { navHostController.navigateToLocation(option) }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.pixelized.rplexicon.ui.navigation
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.composableLexiconDetail
|
||||
import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconSearch
|
||||
import com.pixelized.rplexicon.ui.navigation.screens.composableLocationDetail
|
||||
import com.pixelized.rplexicon.ui.navigation.screens.composableQuestDetail
|
||||
|
||||
val LocalScreenNavHost = staticCompositionLocalOf<NavHostController> {
|
||||
|
|
@ -45,11 +45,10 @@ fun ScreenNavHost(
|
|||
questListState = questListState,
|
||||
locationListState = locationListState
|
||||
)
|
||||
|
||||
composableLexiconDetail()
|
||||
composableLexiconSearch()
|
||||
|
||||
composableQuestDetail()
|
||||
composableLocationDetail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconScreen
|
|||
|
||||
private const val ROUTE = "lexicon"
|
||||
|
||||
const val LEXICON_ROUTE = ROUTE
|
||||
const val LEXICON_LIST_ROUTE = ROUTE
|
||||
|
||||
fun NavGraphBuilder.composableLexicon(
|
||||
lazyListState: LazyListState,
|
||||
) {
|
||||
animatedComposable(
|
||||
route = LEXICON_ROUTE,
|
||||
route = LEXICON_LIST_ROUTE,
|
||||
animation = NavigationAnimation.NONE,
|
||||
) {
|
||||
LexiconScreen(
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import com.pixelized.rplexicon.ui.screens.location.list.LocationScreen
|
|||
|
||||
private const val ROUTE = "locations"
|
||||
|
||||
const val LOCATION_ROUTE = ROUTE
|
||||
const val LOCATION_LIST_ROUTE = ROUTE
|
||||
|
||||
fun NavGraphBuilder.composableLocations(
|
||||
lazyListState: LazyListState,
|
||||
) {
|
||||
animatedComposable(
|
||||
route = LOCATION_ROUTE,
|
||||
route = LOCATION_LIST_ROUTE,
|
||||
animation = NavigationAnimation.NONE,
|
||||
) {
|
||||
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.error.HandleFetchError
|
||||
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.utilitary.extentions.cell
|
||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
|
||||
|
|
@ -59,7 +60,7 @@ fun LocationScreen(
|
|||
refreshState = refresh,
|
||||
refreshing = viewModel.isLoading,
|
||||
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