diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/MapTiles.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/MapTiles.kt index 59e389c0..f068e668 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/MapTiles.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/MapTiles.kt @@ -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) - ) + } } } }