Add location tap feature

This commit is contained in:
Thomas Andres Gomez 2023-08-10 18:27:14 +02:00
parent 6e56313639
commit 81259cabc9
5 changed files with 199 additions and 50 deletions

View file

@ -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,

View file

@ -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 = { },

View file

@ -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,
@ -66,3 +69,4 @@ private fun MarqueeItemPreview() {
)
}
}
}

View file

@ -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,
)
}
}

View 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>