parent
a85530de42
commit
4d78c3c24d
@ -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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user