Add location tap feature
This commit is contained in:
parent
6e56313639
commit
81259cabc9
5 changed files with 199 additions and 50 deletions
|
|
@ -41,6 +41,7 @@ fun FantasyMap(
|
|||
item: State<LocationDetailUio>,
|
||||
selectedItem: State<Int>,
|
||||
onMarquee: (MarqueeUio) -> Unit,
|
||||
onTap: (Offset) -> Unit,
|
||||
) {
|
||||
val colorScheme = MaterialTheme.lexicon.colorScheme
|
||||
val animatedScale = animateFloatAsState(targetValue = state.scale, label = "ScaleAnimation")
|
||||
|
|
@ -84,6 +85,8 @@ fun FantasyMap(
|
|||
.onSizeChanged { state.imageSize = it }
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
|
||||
if (state.freeHand.not()) {
|
||||
item.value.marquees.forEachIndexed { index, item ->
|
||||
if (item.position != Offset.Unspecified) {
|
||||
drawCircle(
|
||||
|
|
@ -114,18 +117,32 @@ fun FantasyMap(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.pointerInput("DetectTapGestures") {
|
||||
detectTapGestures(
|
||||
onTap = { tap ->
|
||||
if (state.freeHand) {
|
||||
onTap(
|
||||
Offset(
|
||||
x = tap.x / size.width,
|
||||
y = tap.y / size.height,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val marquee = item.value.marquees
|
||||
.asReversed()
|
||||
.firstOrNull { item ->
|
||||
if (item.position != Offset.Unspecified) {
|
||||
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) }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
marquee?.let(onMarquee)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
imageModel = { item.value.map },
|
||||
|
|
@ -174,6 +191,9 @@ class FantasyMapState(
|
|||
private val _imageSize: MutableState<IntSize> = mutableStateOf(IntSize.Zero)
|
||||
var imageSize: IntSize by _imageSize
|
||||
|
||||
private val _freeHand: MutableState<Boolean> = mutableStateOf(false)
|
||||
val freeHand: Boolean by _freeHand
|
||||
|
||||
@Stable
|
||||
fun scale(
|
||||
scale: Float,
|
||||
|
|
@ -193,6 +213,10 @@ class FantasyMapState(
|
|||
}
|
||||
}
|
||||
|
||||
fun toggleFreeHand(toggle: Boolean) {
|
||||
_freeHand.value = toggle
|
||||
}
|
||||
|
||||
@Stable
|
||||
fun computeMarqueeOffset(
|
||||
origin: Offset,
|
||||
|
|
|
|||
|
|
@ -22,11 +22,14 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.FilledIconToggleButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
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.SnackbarDuration
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
|
|
@ -48,15 +51,19 @@ 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.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.LocalSnack
|
||||
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.pixelized.rplexicon.utilitary.rememberTextSize
|
||||
import com.skydoves.landscapist.ImageOptions
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.math.max
|
||||
|
|
@ -73,11 +80,16 @@ data class LocationDetailUio(
|
|||
fun LocationDetail(
|
||||
viewModel: LocationDetailViewModel = hiltViewModel()
|
||||
) {
|
||||
val screen = LocalScreenNavHost.current
|
||||
val snack = LocalSnack.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val scroll = rememberScrollState()
|
||||
val pager = rememberPagerState()
|
||||
val fantasy = rememberFantasyMapState()
|
||||
val screen = LocalScreenNavHost.current
|
||||
|
||||
val ok = stringResource(id = android.R.string.ok)
|
||||
val job = remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
val connection = remember {
|
||||
object : NestedScrollConnection {
|
||||
|
|
@ -117,6 +129,19 @@ fun LocationDetail(
|
|||
pager.animateScrollToPage(page = index)
|
||||
}
|
||||
},
|
||||
onMapTap = {
|
||||
job.value?.cancel()
|
||||
job.value = scope.launch {
|
||||
snack.showSnackbar(
|
||||
message = "x:${it.x}, y:${it.y}",
|
||||
actionLabel = ok,
|
||||
duration = SnackbarDuration.Indefinite,
|
||||
)
|
||||
}
|
||||
},
|
||||
onTouch = {
|
||||
fantasy.toggleFreeHand(it)
|
||||
},
|
||||
onCenter = {
|
||||
fantasy.scale(scale = 1f)
|
||||
fantasy.pan(offset = Offset.Zero)
|
||||
|
|
@ -176,10 +201,26 @@ private fun LocationContent(
|
|||
selectedIndex: State<Int>,
|
||||
onBack: () -> Unit,
|
||||
onMarquee: (MarqueeUio) -> Unit,
|
||||
onMapTap: (Offset) -> Unit,
|
||||
onTouch: (Boolean) -> Unit,
|
||||
onCenter: () -> Unit,
|
||||
onZoomIn: () -> Unit,
|
||||
onZoomOut: () -> Unit,
|
||||
) {
|
||||
val itemNameSize = rememberTextSize(style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
val filledIconButtonColors = IconButtonDefaults.filledIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
val filledIconToggleButtonColors = IconButtonDefaults.filledIconToggleButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
checkedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
checkedContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
|
|
@ -226,19 +267,31 @@ private fun LocationContent(
|
|||
item = item,
|
||||
selectedItem = selectedIndex,
|
||||
onMarquee = onMarquee,
|
||||
onTap = onMapTap,
|
||||
)
|
||||
|
||||
FilledIconToggleButton(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.TopEnd)
|
||||
.padding(all = 16.dp),
|
||||
checked = fantasyMapState.freeHand,
|
||||
onCheckedChange = onTouch,
|
||||
colors = filledIconToggleButtonColors,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_touch_app_24),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
colors = filledIconButtonColors,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_remove_24),
|
||||
|
|
@ -247,7 +300,7 @@ private fun LocationContent(
|
|||
}
|
||||
FilledIconButton(
|
||||
onClick = onZoomIn,
|
||||
colors = colors,
|
||||
colors = filledIconButtonColors,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_add_24),
|
||||
|
|
@ -256,7 +309,7 @@ private fun LocationContent(
|
|||
}
|
||||
FilledIconButton(
|
||||
onClick = onCenter,
|
||||
colors = colors,
|
||||
colors = filledIconButtonColors,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_zoom_in_map_24),
|
||||
|
|
@ -273,6 +326,16 @@ private fun LocationContent(
|
|||
.padding(top = 16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterHorizontally)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
text = item.value.name,
|
||||
)
|
||||
|
||||
HorizontalPager(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -287,7 +350,7 @@ private fun LocationContent(
|
|||
MarqueeItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(this@constraint.maxHeight - 32.dp),
|
||||
.height(this@constraint.maxHeight - 32.dp - itemNameSize.height),
|
||||
marquee = marquee,
|
||||
)
|
||||
}
|
||||
|
|
@ -351,6 +414,8 @@ private fun LocationPreview() {
|
|||
selectedIndex = remember { mutableStateOf(0) },
|
||||
onBack = { },
|
||||
onMarquee = { },
|
||||
onMapTap = { },
|
||||
onTouch = { },
|
||||
onCenter = { },
|
||||
onZoomIn = { },
|
||||
onZoomOut = { },
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ 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.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
|
|
@ -39,7 +41,7 @@ fun MarqueeItem(
|
|||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
text = marquee.name,
|
||||
)
|
||||
marquee.description?.let {
|
||||
|
|
@ -56,8 +58,9 @@ fun MarqueeItem(
|
|||
@Preview
|
||||
private fun MarqueeItemPreview() {
|
||||
LexiconTheme {
|
||||
Surface {
|
||||
MarqueeItem(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.padding(all = 16.dp),
|
||||
marquee = MarqueeUio(
|
||||
name = "Name",
|
||||
position = Offset.Zero,
|
||||
|
|
@ -65,4 +68,5 @@ private fun MarqueeItemPreview() {
|
|||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.pixelized.rplexicon.utilitary
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
|
||||
private const val PT_TO_PX_RATIO = 4f / 3f // pt to px ratio
|
||||
|
||||
/**
|
||||
* TextMeasurement class
|
||||
*
|
||||
* Currently it is impossible to set a *minLine* value on a Text in Compose.
|
||||
* This class is a work around that compute the require Height for a [TextStyle].
|
||||
*
|
||||
* google issue tracker :
|
||||
* https://issuetracker.google.com/issues/122476634
|
||||
*
|
||||
* based on :
|
||||
* https://stackoverflow.com/questions/66394624/specify-minimal-lines-for-text-in-jetpack-compose
|
||||
*/
|
||||
@Stable
|
||||
@Immutable
|
||||
data class TextMeasurement(
|
||||
val height: Dp,
|
||||
val line: TextUnit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun rememberTextSize(
|
||||
style: TextStyle,
|
||||
): TextMeasurement {
|
||||
val density = LocalDensity.current
|
||||
return remember(density, style) {
|
||||
val line = if (style.lineHeight != TextUnit.Unspecified) {
|
||||
style.lineHeight
|
||||
} else {
|
||||
style.fontSize * PT_TO_PX_RATIO
|
||||
}
|
||||
val height = with(density) { line.toDp() }
|
||||
|
||||
TextMeasurement(
|
||||
height = height,
|
||||
line = line,
|
||||
)
|
||||
}
|
||||
}
|
||||
5
app/src/main/res/drawable/ic_baseline_touch_app_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_touch_app_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,11.24V7.5C9,6.12 10.12,5 11.5,5S14,6.12 14,7.5v3.74c1.21,-0.81 2,-2.18 2,-3.74C16,5.01 13.99,3 11.5,3S7,5.01 7,7.5C7,9.06 7.79,10.43 9,11.24zM18.84,15.87l-4.54,-2.26c-0.17,-0.07 -0.35,-0.11 -0.54,-0.11H13v-6C13,6.67 12.33,6 11.5,6S10,6.67 10,7.5v10.74c-3.6,-0.76 -3.54,-0.75 -3.67,-0.75c-0.31,0 -0.59,0.13 -0.79,0.33l-0.79,0.8l4.94,4.94C9.96,23.83 10.34,24 10.75,24h6.79c0.75,0 1.33,-0.55 1.44,-1.28l0.75,-5.27c0.01,-0.07 0.02,-0.14 0.02,-0.2C19.75,16.63 19.37,16.09 18.84,15.87z"/>
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue