Fix map tiles in rtl layout

Fix #1333
This commit is contained in:
MM20 2025-04-02 23:26:49 +02:00
parent a85530de42
commit 4d78c3c24d
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389

View File

@ -29,13 +29,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Navigation
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
@ -51,9 +52,11 @@ import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import coil.ImageLoader
@ -134,115 +137,136 @@ fun MapTiles(
} else null
}
BoxWithConstraints(
modifier = modifier,
contentAlignment = Alignment.Center,
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
AnimatedContent(
tileRange,
transitionSpec = {
if (targetState.zoomLevel == initialState.zoomLevel) {
val initialCenterX = (initialState.stop.x + initialState.start.x) / 2f
val targetCenterX = (targetState.stop.x + targetState.start.x) / 2f
val initialCenterY = (initialState.stop.y + initialState.start.y) / 2f
val targetCenterY = (targetState.stop.y + targetState.start.y) / 2f
val initialDeltaX = targetCenterX - initialCenterX
val targetDeltaX = targetCenterX - initialCenterX
val initialDeltaY = targetCenterY - initialCenterY
val targetDeltaY = targetCenterY - initialCenterY
BoxWithConstraints(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
AnimatedContent(
tileRange,
transitionSpec = {
if (targetState.zoomLevel == initialState.zoomLevel) {
val initialCenterX = (initialState.stop.x + initialState.start.x) / 2f
val targetCenterX = (targetState.stop.x + targetState.start.x) / 2f
val initialCenterY = (initialState.stop.y + initialState.start.y) / 2f
val targetCenterY = (targetState.stop.y + targetState.start.y) / 2f
val initialDeltaX = targetCenterX - initialCenterX
val targetDeltaX = targetCenterX - initialCenterX
val initialDeltaY = targetCenterY - initialCenterY
val targetDeltaY = targetCenterY - initialCenterY
return@AnimatedContent slideIn {
IntOffset(
(targetDeltaX * (it.width / tiles.width)).toInt(),
(targetDeltaY * (it.height / tiles.height)).toInt()
)
} togetherWith slideOut {
IntOffset(
-(initialDeltaX * (it.width / tiles.width)).toInt(),
-(initialDeltaY * (it.height / tiles.height)).toInt()
)
}
}
val scale = 2f.pow(targetState.zoomLevel - initialState.zoomLevel)
fadeIn() + scaleIn(initialScale = 1f / scale) togetherWith
fadeOut() + scaleOut(targetScale = scale)
}
) { (start, stop, zoom) ->
var tileWidth by remember { mutableIntStateOf(0) }
Column(modifier = Modifier
.fillMaxWidth()
// Needed to force all tiles to be the _exact_ same size. With weight(1f) we get rounding errors and gaps.
.onSizeChanged { tileWidth = it.width / (stop.x - start.x + 1) }
) {
for (y in start.y..stop.y) {
Row(modifier = Modifier.fillMaxWidth()) {
for (x in start.x..stop.x) {
AsyncImage(
modifier = Modifier
.width(tileWidth.toDp())
.height(tileWidth.toDp())
.background(MaterialTheme.colorScheme.secondaryContainer),
imageLoader = MapTileLoader.loader,
model = MapTileLoader.getTileRequest(tileServerUrl, x, y, zoom),
contentDescription = null,
colorFilter = colorMatrix?.let { ColorFilter.colorMatrix(it) },
filterQuality = FilterQuality.High,
return@AnimatedContent slideIn {
IntOffset(
(targetDeltaX * (it.width / tiles.width)).toInt(),
(targetDeltaY * (it.height / tiles.height)).toInt()
)
} togetherWith slideOut {
IntOffset(
-(initialDeltaX * (it.width / tiles.width)).toInt(),
-(initialDeltaY * (it.height / tiles.height)).toInt()
)
}
}
val scale = 2f.pow(targetState.zoomLevel - initialState.zoomLevel)
fadeIn() + scaleIn(initialScale = 1f / scale) togetherWith
fadeOut() + scaleOut(targetScale = scale)
}
) { (start, stop, zoom) ->
var tileWidth by remember { mutableIntStateOf(0) }
Column(
modifier = Modifier
.fillMaxWidth()
// Needed to force all tiles to be the _exact_ same size. With weight(1f) we get rounding errors and gaps.
.onSizeChanged { tileWidth = it.width / (stop.x - start.x + 1) }
) {
for (y in start.y..stop.y) {
Row(modifier = Modifier.fillMaxWidth()) {
for (x in start.x..stop.x) {
AsyncImage(
modifier = Modifier
.width(tileWidth.toDp())
.height(tileWidth.toDp())
.background(MaterialTheme.colorScheme.secondaryContainer),
imageLoader = MapTileLoader.loader,
model = MapTileLoader.getTileRequest(tileServerUrl, x, y, zoom),
contentDescription = null,
colorFilter = colorMatrix?.let { ColorFilter.colorMatrix(it) },
filterQuality = FilterQuality.High,
)
}
}
}
}
}
val tileSize = minWidth / tiles.width.toFloat()
val locationTileCoordinates =
getTileCoordinates(location.latitude, location.longitude, zoom)
val tileSize = minWidth / tiles.width.toFloat()
val locationTileCoordinates =
getTileCoordinates(location.latitude, location.longitude, zoom)
Box(
modifier = Modifier
.align(Alignment.TopStart)
.width(12.dp)
.height(4.dp)
.absoluteOffset(
x = (locationTileCoordinates.x - start.x) * tileSize - 6.dp,
y = (locationTileCoordinates.y - start.y) * tileSize - 2.dp,
)
.shadow(1.dp, CircleShape)
)
Icon(
imageVector = Icons.Rounded.Place,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier
.align(Alignment.TopStart)
.size(24.dp)
.absoluteOffset(
x = (locationTileCoordinates.x - start.x) * tileSize - 12.dp,
y = (locationTileCoordinates.y - start.y) * tileSize - 22.dp,
)
)
if (userLocation != null) {
val userIndicatorOffset by animateOffsetAsState(
targetValue = getTileCoordinates(userLocation.lat, userLocation.lon, zoom).let {
Offset(
(it.x - start.x) * tileSize.value - 8f,
(it.y - start.y) * tileSize.value - 8f
Box(
modifier = Modifier
.align(Alignment.TopStart)
.width(12.dp)
.height(4.dp)
.absoluteOffset(
x = (locationTileCoordinates.x - start.x) * tileSize - 6.dp,
y = (locationTileCoordinates.y - start.y) * tileSize - 2.dp,
)
},
animationSpec = tween()
.shadow(1.dp, CircleShape)
)
if (userLocation.heading != null) {
val headingAnim by animateValueAsState(
targetValue = userLocation.heading,
typeConverter = Float.DegreesConverter
Icon(
imageVector = Icons.Rounded.Place,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier
.align(Alignment.TopStart)
.size(24.dp)
.absoluteOffset(
x = (locationTileCoordinates.x - start.x) * tileSize - 12.dp,
y = (locationTileCoordinates.y - start.y) * tileSize - 22.dp,
)
)
if (userLocation != null) {
val userIndicatorOffset by animateOffsetAsState(
targetValue = getTileCoordinates(
userLocation.lat,
userLocation.lon,
zoom
).let {
Offset(
(it.x - start.x) * tileSize.value - 8f,
(it.y - start.y) * tileSize.value - 8f
)
},
animationSpec = tween()
)
Icon(
imageVector = Icons.Rounded.Navigation,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
if (userLocation.heading != null) {
val headingAnim by animateValueAsState(
targetValue = userLocation.heading,
typeConverter = Float.DegreesConverter
)
Icon(
imageVector = Icons.Rounded.Navigation,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier
.align(Alignment.TopStart)
.size(16.dp)
.absoluteOffset(
userIndicatorOffset.x.dp,
userIndicatorOffset.y.dp
)
.rotate(headingAnim)
.absoluteOffset(y = -8.dp)
)
}
Box(
modifier = Modifier
.align(Alignment.TopStart)
.size(16.dp)
@ -250,34 +274,26 @@ fun MapTiles(
userIndicatorOffset.x.dp,
userIndicatorOffset.y.dp
)
.rotate(headingAnim)
.absoluteOffset(y = -8.dp)
.background(MaterialTheme.colorScheme.tertiary, CircleShape)
.border(2.dp, MaterialTheme.colorScheme.onTertiary, CircleShape)
.shadow(1.dp, CircleShape)
)
}
Box(
modifier = Modifier
.align(Alignment.TopStart)
.size(16.dp)
.absoluteOffset(
userIndicatorOffset.x.dp,
userIndicatorOffset.y.dp
if (osmAttribution != null) {
Text(
text = osmAttribution,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.align(Alignment.BottomEnd)
.background(
MaterialTheme.colorScheme.surfaceContainerHigh.copy(
alpha = .5f
)
)
.padding(top = 2.dp, bottom = 2.dp, start = 4.dp, end = 4.dp)
)
.background(MaterialTheme.colorScheme.tertiary, CircleShape)
.border(2.dp, MaterialTheme.colorScheme.onTertiary, CircleShape)
.shadow(1.dp, CircleShape)
)
if (osmAttribution != null) {
Text(
text = osmAttribution,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.align(Alignment.BottomEnd)
.background(MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = .5f))
.padding(top = 2.dp, bottom = 2.dp, start = 4.dp, end = 4.dp)
)
}
}
}
}