Start implementing LLS timeline item
This commit is contained in:
parent
a22c9871e3
commit
b082f59f9c
16 changed files with 356 additions and 121 deletions
|
|
@ -9,7 +9,9 @@
|
|||
package io.element.android.features.location.api
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
|
@ -22,6 +24,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.Extras
|
||||
import coil3.compose.AsyncImagePainter
|
||||
|
|
@ -38,68 +42,131 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||
|
||||
/**
|
||||
* Shows a static map image downloaded via a third party service's static maps API.
|
||||
*
|
||||
* Handles 4 distinct cases:
|
||||
* 1. Stale location (pinVariant is StaleLocation) - shows stale map with stale pin, no fetching
|
||||
* 2. Null location - shows blurred placeholder, no pin, no loading
|
||||
* 3. Loading (location != null, fetching) - shows blurred placeholder with loading indicator
|
||||
* 4. Success (location != null, loaded) - shows actual map with pin
|
||||
*/
|
||||
@Composable
|
||||
fun StaticMapView(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
location: Location?,
|
||||
zoom: Double,
|
||||
pinVariant: PinVariant,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
darkMode: Boolean = !ElementTheme.isLightTheme,
|
||||
) {
|
||||
// Using BoxWithConstraints to:
|
||||
// 1) Size the inner Image to the same Dp size of the outer BoxWithConstraints.
|
||||
// 2) Request the static map image of the exact required size in Px to fill the AsyncImage.
|
||||
BoxWithConstraints(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var retryHash by remember { mutableIntStateOf(0) }
|
||||
val builder = remember { StaticMapUrlBuilder() }
|
||||
val painter = rememberAsyncImagePainter(
|
||||
model = if (constraints.isZero) {
|
||||
// Avoid building a URL if any of the size constraints is zero (else it will thrown an exception).
|
||||
null
|
||||
} else {
|
||||
ImageRequest.Builder(context)
|
||||
.data(
|
||||
builder.build(
|
||||
lat = lat,
|
||||
lon = lon,
|
||||
zoom = zoom,
|
||||
darkMode = darkMode,
|
||||
width = constraints.maxWidth,
|
||||
height = constraints.maxHeight,
|
||||
density = LocalDensity.current.density,
|
||||
)
|
||||
)
|
||||
.size(width = constraints.maxWidth, height = constraints.maxHeight)
|
||||
.apply {
|
||||
extras.set(Extras.Key("retry_hash"), retryHash).build()
|
||||
}
|
||||
.build()
|
||||
// Case 1: Stale location - show stale map with stale pin, no fetching
|
||||
when {
|
||||
pinVariant is PinVariant.StaleLocation -> {
|
||||
StaleMapContent(
|
||||
pinVariant = pinVariant,
|
||||
contentDescription = contentDescription,
|
||||
width = maxWidth,
|
||||
height = maxHeight,
|
||||
)
|
||||
}
|
||||
)
|
||||
// Case 2: Null location - show blurred placeholder, no pin, no loading
|
||||
location == null -> {
|
||||
StaticMapPlaceholder(
|
||||
painter = painterResource(R.drawable.blurred_map),
|
||||
canReload = false,
|
||||
contentDescription = contentDescription,
|
||||
width = maxWidth,
|
||||
height = maxHeight,
|
||||
onLoadMapClick = {}
|
||||
)
|
||||
}
|
||||
// Cases 3 & 4: Non-null location - fetch map
|
||||
else -> LoadableMapContent(
|
||||
location = location,
|
||||
zoom = zoom,
|
||||
pinVariant = pinVariant,
|
||||
contentDescription = contentDescription,
|
||||
darkMode = darkMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val collectedState = painter.state.collectAsState()
|
||||
if (collectedState.value is AsyncImagePainter.State.Success) {
|
||||
@Composable
|
||||
private fun BoxWithConstraintsScope.StaleMapContent(
|
||||
pinVariant: PinVariant,
|
||||
contentDescription: String?,
|
||||
width: Dp,
|
||||
height: Dp,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.stale_map),
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
modifier = Modifier.size(width = width, height = height)
|
||||
)
|
||||
LocationPin(variant = pinVariant, modifier = Modifier.centerBottomEdge(this@StaleMapContent))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxWithConstraintsScope.LoadableMapContent(
|
||||
location: Location,
|
||||
zoom: Double,
|
||||
pinVariant: PinVariant,
|
||||
contentDescription: String?,
|
||||
darkMode: Boolean,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var retryHash by remember { mutableIntStateOf(0) }
|
||||
val builder = remember { StaticMapUrlBuilder() }
|
||||
|
||||
val painter = rememberAsyncImagePainter(
|
||||
model = if (constraints.isZero) {
|
||||
// Avoid building a URL if any of the size constraints is zero
|
||||
null
|
||||
} else {
|
||||
ImageRequest.Builder(context)
|
||||
.data(
|
||||
builder.build(
|
||||
lat = location.lat,
|
||||
lon = location.lon,
|
||||
zoom = zoom,
|
||||
darkMode = darkMode,
|
||||
width = constraints.maxWidth,
|
||||
height = constraints.maxHeight,
|
||||
density = LocalDensity.current.density,
|
||||
)
|
||||
)
|
||||
.size(width = constraints.maxWidth, height = constraints.maxHeight)
|
||||
.apply {
|
||||
extras.set(Extras.Key("retry_hash"), retryHash).build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
)
|
||||
|
||||
val state by painter.state.collectAsState()
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(width = maxWidth, height = maxHeight),
|
||||
// The returned image can be smaller than the requested size due to the static maps API having
|
||||
// a max width and height of 2048 px. See buildStaticMapsApiUrl() for more details.
|
||||
// We apply ContentScale.Fit to scale the image to fill the AsyncImage should this be the case.
|
||||
// a max width and height of 2048 px. We apply ContentScale.Fit to handle this.
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
LocationPin(variant = pinVariant, modifier = Modifier.centerBottomEdge(this))
|
||||
} else {
|
||||
}
|
||||
else -> {
|
||||
StaticMapPlaceholder(
|
||||
showProgress = collectedState.value.isLoading(),
|
||||
canReload = builder.isServiceAvailable(),
|
||||
painter = painterResource(R.drawable.blurred_map),
|
||||
canReload = builder.isServiceAvailable() && state is AsyncImagePainter.State.Error,
|
||||
contentDescription = contentDescription,
|
||||
width = maxWidth,
|
||||
height = maxHeight,
|
||||
|
|
@ -109,17 +176,11 @@ fun StaticMapView(
|
|||
}
|
||||
}
|
||||
|
||||
private fun AsyncImagePainter.State.isLoading(): Boolean {
|
||||
return this is AsyncImagePainter.State.Empty ||
|
||||
this is AsyncImagePainter.State.Loading
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun StaticMapViewPreview() = ElementPreview {
|
||||
StaticMapView(
|
||||
lat = 0.0,
|
||||
lon = 0.0,
|
||||
location = Location(0.0, 0.0),
|
||||
zoom = 0.0,
|
||||
contentDescription = null,
|
||||
pinVariant = PinVariant.PinnedLocation,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -34,7 +35,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
|
||||
@Composable
|
||||
internal fun StaticMapPlaceholder(
|
||||
showProgress: Boolean,
|
||||
painter: Painter,
|
||||
canReload: Boolean,
|
||||
contentDescription: String?,
|
||||
width: Dp,
|
||||
|
|
@ -46,17 +47,15 @@ internal fun StaticMapPlaceholder(
|
|||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.size(width = width, height = height)
|
||||
.then(if (showProgress) Modifier else Modifier.clickable(onClick = onLoadMapClick))
|
||||
.clickable(enabled = canReload, onClick = onLoadMapClick)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.blurred_map),
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
modifier = Modifier.size(width = width, height = height)
|
||||
)
|
||||
if (showProgress) {
|
||||
CircularProgressIndicator()
|
||||
} else if (canReload) {
|
||||
if (canReload) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
|
|
@ -77,13 +76,10 @@ internal fun StaticMapPlaceholderPreview() = ElementPreview {
|
|||
modifier = Modifier.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
listOf(
|
||||
true to false,
|
||||
false to true,
|
||||
false to false,
|
||||
).forEach { (showProgress, canReload) ->
|
||||
listOf(false, true)
|
||||
.forEach { canReload ->
|
||||
StaticMapPlaceholder(
|
||||
showProgress = showProgress,
|
||||
painter = painterResource(R.drawable.blurred_map),
|
||||
canReload = canReload,
|
||||
contentDescription = null,
|
||||
width = 400.dp,
|
||||
|
|
|
|||
BIN
features/location/api/src/main/res/drawable-night/stale_map.png
Normal file
BIN
features/location/api/src/main/res/drawable-night/stale_map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
features/location/api/src/main/res/drawable/stale_map.png
Normal file
BIN
features/location/api/src/main/res/drawable/stale_map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Loading…
Add table
Add a link
Reference in a new issue