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.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Navigation import androidx.compose.material.icons.rounded.Navigation
import androidx.compose.material.icons.rounded.Place import androidx.compose.material.icons.rounded.Place
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember 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.graphics.FilterQuality
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times import androidx.compose.ui.unit.times
import coil.ImageLoader import coil.ImageLoader
@ -134,115 +137,136 @@ fun MapTiles(
} else null } else null
} }
BoxWithConstraints( CompositionLocalProvider(
modifier = modifier, LocalLayoutDirection provides LayoutDirection.Ltr
contentAlignment = Alignment.Center,
) { ) {
AnimatedContent( BoxWithConstraints(
tileRange, modifier = modifier,
transitionSpec = { contentAlignment = Alignment.Center,
if (targetState.zoomLevel == initialState.zoomLevel) { ) {
val initialCenterX = (initialState.stop.x + initialState.start.x) / 2f AnimatedContent(
val targetCenterX = (targetState.stop.x + targetState.start.x) / 2f tileRange,
val initialCenterY = (initialState.stop.y + initialState.start.y) / 2f transitionSpec = {
val targetCenterY = (targetState.stop.y + targetState.start.y) / 2f if (targetState.zoomLevel == initialState.zoomLevel) {
val initialDeltaX = targetCenterX - initialCenterX val initialCenterX = (initialState.stop.x + initialState.start.x) / 2f
val targetDeltaX = targetCenterX - initialCenterX val targetCenterX = (targetState.stop.x + targetState.start.x) / 2f
val initialDeltaY = targetCenterY - initialCenterY val initialCenterY = (initialState.stop.y + initialState.start.y) / 2f
val targetDeltaY = targetCenterY - initialCenterY 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 { return@AnimatedContent slideIn {
IntOffset( IntOffset(
(targetDeltaX * (it.width / tiles.width)).toInt(), (targetDeltaX * (it.width / tiles.width)).toInt(),
(targetDeltaY * (it.height / tiles.height)).toInt() (targetDeltaY * (it.height / tiles.height)).toInt()
) )
} togetherWith slideOut { } togetherWith slideOut {
IntOffset( IntOffset(
-(initialDeltaX * (it.width / tiles.width)).toInt(), -(initialDeltaX * (it.width / tiles.width)).toInt(),
-(initialDeltaY * (it.height / tiles.height)).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 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 tileSize = minWidth / tiles.width.toFloat()
val locationTileCoordinates = val locationTileCoordinates =
getTileCoordinates(location.latitude, location.longitude, zoom) getTileCoordinates(location.latitude, location.longitude, zoom)
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.TopStart) .align(Alignment.TopStart)
.width(12.dp) .width(12.dp)
.height(4.dp) .height(4.dp)
.absoluteOffset( .absoluteOffset(
x = (locationTileCoordinates.x - start.x) * tileSize - 6.dp, x = (locationTileCoordinates.x - start.x) * tileSize - 6.dp,
y = (locationTileCoordinates.y - start.y) * tileSize - 2.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
) )
}, .shadow(1.dp, CircleShape)
animationSpec = tween()
) )
if (userLocation.heading != null) { Icon(
val headingAnim by animateValueAsState( imageVector = Icons.Rounded.Place,
targetValue = userLocation.heading, contentDescription = null,
typeConverter = Float.DegreesConverter 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( if (userLocation.heading != null) {
imageVector = Icons.Rounded.Navigation, val headingAnim by animateValueAsState(
contentDescription = null, targetValue = userLocation.heading,
tint = MaterialTheme.colorScheme.tertiary, 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 modifier = Modifier
.align(Alignment.TopStart) .align(Alignment.TopStart)
.size(16.dp) .size(16.dp)
@ -250,34 +274,26 @@ fun MapTiles(
userIndicatorOffset.x.dp, userIndicatorOffset.x.dp,
userIndicatorOffset.y.dp userIndicatorOffset.y.dp
) )
.rotate(headingAnim) .background(MaterialTheme.colorScheme.tertiary, CircleShape)
.absoluteOffset(y = -8.dp) .border(2.dp, MaterialTheme.colorScheme.onTertiary, CircleShape)
.shadow(1.dp, CircleShape)
) )
}
Box( if (osmAttribution != null) {
modifier = Modifier Text(
.align(Alignment.TopStart) text = osmAttribution,
.size(16.dp) style = MaterialTheme.typography.labelSmall,
.absoluteOffset( color = MaterialTheme.colorScheme.onSurface,
userIndicatorOffset.x.dp, modifier = Modifier
userIndicatorOffset.y.dp .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)
)
} }
} }
} }