Skip to content

Commit 5df257f

Browse files
sorenoidSoren Rotheri9000
authored
merge Feature branches/popup to v.next (#517)
* `Popup`: component and app (#406) * add Popup component and microapp * add bottom sheet and api file. * add READMEs * address review feedback * change name in android manifest that were changed in previous commit * Update microapps/PopupApp/README.md Co-authored-by: Erick Lopez Solis <[email protected]> * Update toolkit/popup/README.md Co-authored-by: Erick Lopez Solis <[email protected]> * add initialization support to the Popup composable. --------- Co-authored-by: Soren Roth <[email protected]> Co-authored-by: Erick Lopez Solis <[email protected]> * `Popup`: Adds TextPopupElement Composable (#413) * `Popup`: state collection (#420) * pull compose bom into androidTest and test Implementations when needed. Add missing version for artifact not covered by compose bom (androidx-activity:activity-compose) * Address warnings from version upgrades. pull compose bom into androidTest and test Implementations when needed. Add missing version for artifact not covered by compose bom (androidx-activity:activity-compose). * preliminary setup of state collection. * add initialization support to the Popup composable. * workaround for known issue in gradle task graph progress. opt in for experimental coroutine API * view model added to the Popup App. * move files around. make state objects Parcelable. * ensure states are not created until expressions are evaluated. * Update toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt Co-authored-by: Erick Lopez Solis <[email protected]> * remove Parcelable super class from base state class * remove comment --------- Co-authored-by: Soren Roth <[email protected]> Co-authored-by: Erick Lopez Solis <[email protected]> * update expandable card (#426) * pull compose bom into androidTest and test Implementations when needed. Add missing version for artifact not covered by compose bom (androidx-activity:activity-compose) * Address warnings from version upgrades. pull compose bom into androidTest and test Implementations when needed. Add missing version for artifact not covered by compose bom (androidx-activity:activity-compose). * preliminary setup of state collection. * add initialization support to the Popup composable. * workaround for known issue in gradle task graph progress. opt in for experimental coroutine API * view model added to the Popup App. * move files around. make state objects Parcelable. * ensure states are not created until expressions are evaluated. * Update toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt Co-authored-by: Erick Lopez Solis <[email protected]> * remove Parcelable super class from base state class * make expandable card more like field maps. * add defaults * address PR feedback * fix formatting * default to expandable card expanded --------- Co-authored-by: Soren Roth <[email protected]> Co-authored-by: Erick Lopez Solis <[email protected]> * `Popup` attachment element (#430) * pull compose bom into androidTest and test Implementations when needed. Add missing version for artifact not covered by compose bom (androidx-activity:activity-compose) * Address warnings from version upgrades. pull compose bom into androidTest and test Implementations when needed. Add missing version for artifact not covered by compose bom (androidx-activity:activity-compose). * preliminary setup of state collection. * add initialization support to the Popup composable. * workaround for known issue in gradle task graph progress. opt in for experimental coroutine API * view model added to the Popup App. * move files around. make state objects Parcelable. * ensure states are not created until expressions are evaluated. * attachment initial support * Revert "attachment initial support" This reverts commit c888b6b. * attachment initial support * Atachments hooked up to state collection and rendering in the Popup. * remove file from other branch * Update toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt Co-authored-by: Erick Lopez Solis <[email protected]> * remove Parcelable super class from base state class * make expandable card more like field maps. * add defaults * address PR feedback * fix formatting * default to expandable card expanded * merge in expandable card refactors * support document and other content types. * add all shapes sizes and color to theme defaults object * remove newlines * remove element count, add toggleable parameter to expandable card --------- Co-authored-by: Soren Roth <[email protected]> Co-authored-by: Erick Lopez Solis <[email protected]> * `Popup:` Adds FieldsPopupElement composable (#428) * Adds ViewableFile implementation (#445) * popup media element and media image thumbnail support (#449) * Media element images wip * Media element with support for images * use colors from defaults * fix capitalization --------- Co-authored-by: Soren Roth <[email protected]> * `Popup`: chart images (#452) * Media element images wip * Media element with support for images * chart image support, dark mode support for media. * update SDK dependency for Popups * tweak padding of charts vs images --------- Co-authored-by: Soren Roth <[email protected]> * `Popup:` Fixes TextPopupElement composable shifting in size when coming back into visibility (#461) * `Popup:` Adds a file viewer for image attachments (#455) * `Popup:` Adds support for viewing videos and audio in FileViewer (#464) * Adds support for opening files in the system file viewer (#472) * chart support for AsyncImage from saved files (#475) * chart support for AsyncImage from saved files * remove unused lambda. update comment --------- Co-authored-by: Soren Roth <[email protected]> * attachment instance id workaround (#483) * chart support for AsyncImage from saved files * remove unused lambda. update comment * d * copy attachments on first fetch and pass down into the composition --------- Co-authored-by: Soren Roth <[email protected]> * `Popup:` Adds support for viewing media in file viewer (#481) * popup dynamic entity (#492) * wip * Basic support for DynamicEntity in Popups * minor formatting * merge in media FileViewer * only animate the fields element when the dynamic entity pulses -- for now. * remove unused named lambda parameter. remove newline --------- Co-authored-by: Soren Roth <[email protected]> * dont recreate the media image if it has already been saved to disk (#497) Co-authored-by: Soren Roth <[email protected]> * remove space. add gradle stuff for tests * add Popup to t9manifest * update api file * some small fixes (#508) Co-authored-by: Soren Roth <[email protected]> * set default map to fourteeners. * popup style completeness (#509) * provide expandable card text color * Set the color of the Popup title text --------- Co-authored-by: Soren Roth <[email protected]> * update dynamic charts and images (#515) * consistent animation. todo: push animation down to components * push animation down to components. refactor PopupMediaState creation to use factories. * add refreshInterval support. * reset map to fourteeners * feedback. property name, cleanup --------- Co-authored-by: Soren Roth <[email protected]> --------- Co-authored-by: Soren Roth <[email protected]> Co-authored-by: Erick Lopez Solis <[email protected]>
1 parent be18381 commit 5df257f

File tree

7 files changed

+215
-127
lines changed

7 files changed

+215
-127
lines changed

toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt

+19-28
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@
1818

1919
package com.arcgismaps.toolkit.popup
2020

21-
import androidx.compose.animation.AnimatedVisibility
22-
import androidx.compose.animation.core.Spring
2321
import androidx.compose.animation.core.animateFloatAsState
24-
import androidx.compose.animation.core.spring
25-
import androidx.compose.animation.fadeIn
26-
import androidx.compose.animation.fadeOut
2722
import androidx.compose.foundation.layout.Column
2823
import androidx.compose.foundation.layout.Spacer
2924
import androidx.compose.foundation.layout.fillMaxSize
@@ -34,13 +29,15 @@ import androidx.compose.foundation.lazy.LazyColumn
3429
import androidx.compose.foundation.lazy.rememberLazyListState
3530
import androidx.compose.material3.HorizontalDivider
3631
import androidx.compose.material3.LinearProgressIndicator
32+
import androidx.compose.material3.MaterialTheme
3733
import androidx.compose.material3.Surface
3834
import androidx.compose.material3.Text
3935
import androidx.compose.runtime.Composable
4036
import androidx.compose.runtime.Immutable
4137
import androidx.compose.runtime.LaunchedEffect
4238
import androidx.compose.runtime.Stable
4339
import androidx.compose.runtime.getValue
40+
import androidx.compose.runtime.mutableLongStateOf
4441
import androidx.compose.runtime.mutableStateOf
4542
import androidx.compose.runtime.remember
4643
import androidx.compose.runtime.rememberCoroutineScope
@@ -75,7 +72,6 @@ import com.arcgismaps.toolkit.popup.internal.element.textelement.TextPopupElemen
7572
import com.arcgismaps.toolkit.popup.internal.element.textelement.rememberTextElementState
7673
import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.FileViewer
7774
import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ViewableFile
78-
import kotlinx.coroutines.delay
7975

8076
@Immutable
8177
private data class PopupState(@Stable val popup: Popup)
@@ -117,16 +113,14 @@ private fun Popup(popupState: PopupState, modifier: Modifier = Modifier) {
117113
val dynamicEntity = (popup.geoElement as? DynamicEntity)
118114
var evaluated by rememberSaveable(popup) { mutableStateOf(false) }
119115
var fetched by rememberSaveable(popup) { mutableStateOf(false) }
120-
var refreshed by rememberSaveable(dynamicEntity) { mutableStateOf(true) }
116+
var lastUpdatedEntityId by rememberSaveable(dynamicEntity) { mutableLongStateOf(dynamicEntity?.id ?: -1) }
121117
if (dynamicEntity != null) {
122118
LaunchedEffect(popup) {
123119
dynamicEntity.dynamicEntityChangedEvent.collect {
124-
refreshed = false
125120
// briefly show the initializing screen so it is clear the entity just pulsed
126121
// and values may have changed.
127-
delay(300)
128122
popupState.popup.evaluateExpressions()
129-
refreshed = true
123+
lastUpdatedEntityId = it.receivedObservation?.id ?: -1
130124
}
131125
}
132126
}
@@ -149,11 +143,11 @@ private fun Popup(popupState: PopupState, modifier: Modifier = Modifier) {
149143
evaluated = true
150144
}
151145

152-
Popup(popupState, evaluated && fetched, refreshed)
146+
Popup(popupState, evaluated && fetched, lastUpdatedEntityId)
153147
}
154148

155149
@Composable
156-
private fun Popup(popupState: PopupState, initialized: Boolean, refreshed: Boolean, modifier: Modifier = Modifier) {
150+
private fun Popup(popupState: PopupState, initialized: Boolean, refreshed: Long, modifier: Modifier = Modifier) {
157151
val scope = rememberCoroutineScope()
158152
val popup = popupState.popup
159153
val viewableFileState = rememberSaveable { mutableStateOf<ViewableFile?>(null) }
@@ -168,6 +162,7 @@ private fun Popup(popupState: PopupState, initialized: Boolean, refreshed: Boole
168162
) {
169163
Text(
170164
text = popup.title,
165+
color = MaterialTheme.colorScheme.onBackground,
171166
modifier = Modifier.padding(horizontal = 15.dp)
172167
)
173168
Spacer(
@@ -187,16 +182,22 @@ private fun Popup(popupState: PopupState, initialized: Boolean, refreshed: Boole
187182
}
188183
}
189184

185+
/**
186+
* The body of the Popup composable
187+
*
188+
* @param popupState the immutable state object containing the Popup.
189+
* @param refreshed indicates that a new evaluation of elements has occurred. Only for DynamicEntity
190+
* @param onFileClicked the callback to display an attachment or media image
191+
*/
190192
@Composable
191193
private fun PopupBody(
192194
popupState: PopupState,
193-
refreshed: Boolean,
195+
refreshed: Long,
194196
onFileClicked: (ViewableFile) -> Unit = {}
195197
) {
196198
val popup = popupState.popup
197199
val lazyListState = rememberLazyListState()
198200
val states = rememberStates(popup, attachments)
199-
200201
LazyColumn(
201202
modifier = Modifier
202203
.fillMaxSize()
@@ -226,20 +227,10 @@ private fun PopupBody(
226227

227228
is FieldsPopupElement -> {
228229
item(contentType = FieldsPopupElement::class.java) {
229-
AnimatedVisibility(
230-
visible = refreshed,
231-
enter = fadeIn(
232-
animationSpec = spring(stiffness = Spring.StiffnessHigh)
233-
),
234-
exit = fadeOut(
235-
animationSpec = spring(stiffness = Spring.StiffnessLow),
236-
targetAlpha = 0.5f
237-
)
238-
) {
239-
FieldsPopupElement(
240-
entry.state as FieldsElementState,
241-
)
242-
}
230+
FieldsPopupElement(
231+
state = entry.state as FieldsElementState,
232+
refreshed = refreshed
233+
)
243234
}
244235
}
245236

toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsPopupElement.kt

+21-2
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@ package com.arcgismaps.toolkit.popup.internal.element.fieldselement
1818
import android.content.Intent
1919
import android.net.Uri
2020
import android.util.Log
21+
import androidx.compose.animation.core.Animatable
22+
import androidx.compose.animation.core.TweenSpec
2123
import androidx.compose.foundation.background
2224
import androidx.compose.foundation.layout.Column
2325
import androidx.compose.foundation.text.ClickableText
2426
import androidx.compose.material3.ListItem
2527
import androidx.compose.material3.MaterialTheme
2628
import androidx.compose.material3.Text
2729
import androidx.compose.runtime.Composable
30+
import androidx.compose.runtime.LaunchedEffect
31+
import androidx.compose.runtime.remember
2832
import androidx.compose.ui.Modifier
2933
import androidx.compose.ui.graphics.Color
34+
import androidx.compose.ui.graphics.graphicsLayer
3035
import androidx.compose.ui.platform.LocalContext
3136
import androidx.compose.ui.text.SpanStyle
3237
import androidx.compose.ui.text.buildAnnotatedString
@@ -41,11 +46,25 @@ import com.arcgismaps.toolkit.popup.internal.ui.ExpandableCard
4146
* @since 200.5.0
4247
*/
4348
@Composable
44-
internal fun FieldsPopupElement(state: FieldsElementState) {
49+
internal fun FieldsPopupElement(
50+
state: FieldsElementState,
51+
refreshed: Long
52+
) {
53+
val alphaAnimation = remember(refreshed) {
54+
Animatable(0f)
55+
}
56+
57+
LaunchedEffect(refreshed) {
58+
alphaAnimation.animateTo(1f, animationSpec = TweenSpec(durationMillis = 1000))
59+
}
60+
4561
val localContext = LocalContext.current
4662
ExpandableCard(
4763
title = state.title,
4864
description = state.description,
65+
modifier = Modifier.graphicsLayer {
66+
alpha = alphaAnimation.value
67+
}
4968
) {
5069
Column {
5170
state.fieldsToFormattedValues.forEach {
@@ -105,6 +124,6 @@ private fun FieldsPopupElementPreview() {
105124
),
106125
id = 0
107126
)
108-
FieldsPopupElement(state)
127+
FieldsPopupElement(state, 0L)
109128
}
110129

toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt

+105-46
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,16 @@ import com.arcgismaps.mapping.popup.MediaPopupElement
3939
import com.arcgismaps.mapping.popup.Popup
4040
import com.arcgismaps.mapping.popup.PopupMedia
4141
import com.arcgismaps.mapping.popup.PopupMediaType
42+
import com.arcgismaps.realtime.DynamicEntity
4243
import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementState
4344
import com.arcgismaps.toolkit.popup.internal.util.MediaImageProvider
4445
import kotlinx.coroutines.CoroutineScope
46+
import kotlinx.coroutines.Dispatchers
47+
import kotlinx.coroutines.delay
4548
import kotlinx.coroutines.launch
46-
import java.util.Objects
49+
import kotlinx.coroutines.withContext
50+
import java.io.File
51+
import java.util.UUID
4752

4853
/**
4954
* Represents the state of an [MediaPopupElement]
@@ -67,55 +72,32 @@ internal class MediaElementState(
6772
description = mediaPopupElement.description,
6873
title = mediaPopupElement.title,
6974
media = mediaPopupElement.media.mapIndexed { index, media ->
75+
val model = models.getOrNull(index) ?: ""
7076
if (media.type.isChart) {
71-
PopupMediaState(
72-
media,
73-
scope,
74-
if (models.size > index) {
75-
models[index]
76-
} else {
77-
""
78-
},
79-
MediaImageProvider(
80-
fileName = "media-${Objects.hash(mediaPopupElement.title, media.title, media.caption)}.png",
81-
folderName = mediaFolder
82-
) {
83-
media.generateChart(chartParams).getOrThrow().image.bitmap
84-
}
85-
)
86-
} else if (media.type is PopupMediaType.Image) {
87-
val srcUrl = media.value?.sourceUrl
88-
?: throw IllegalArgumentException("null sourceUrl for popup media")
89-
PopupMediaState(
90-
media,
91-
scope,
92-
if (models.size > index) {
93-
models[index]
94-
} else {
95-
""
96-
},
97-
MediaImageProvider(
98-
fileName = "media-${Objects.hash(srcUrl)}",
99-
folderName = mediaFolder
100-
) {
101-
val request = ImageRequest.Builder(context)
102-
.data(srcUrl)
103-
.crossfade(true)
104-
.build()
105-
(context.imageLoader.execute(request).drawable as? BitmapDrawable)?.bitmap
106-
?: throw IllegalStateException("couldn't load image at $srcUrl")
107-
}
108-
)
109-
77+
PopupMediaState.createChartMediaState(media, model, scope, mediaFolder, chartParams)
11078
} else {
111-
throw IllegalArgumentException("unknown media type")
79+
PopupMediaState.createImageMediaState(media, model, scope, mediaFolder, context)
11280
}
113-
11481
}
11582
)
11683

84+
/**
85+
* Update the PopupMedia so that a new chart image can be acquired. Only necessary
86+
* for DynamicEntity Popups.
87+
*
88+
* @param newElement the new MediaPopupElement which contains the new PopupMedia
89+
* @param scope the current CoroutineScope of the Composition.
90+
*/
91+
internal fun updateMediaElement(newElement: MediaPopupElement, scope: CoroutineScope) {
92+
newElement.media.forEachIndexed { index, medium ->
93+
if (medium.type.isChart) {
94+
media.getOrNull(index)?.updateMedia(medium, scope)
95+
}
96+
}
97+
}
98+
11799
companion object {
118-
fun Saver(
100+
internal fun Saver(
119101
element: MediaPopupElement,
120102
scope: CoroutineScope,
121103
chartFolder: String,
@@ -183,6 +165,13 @@ internal fun rememberMediaElementState(
183165
chartParams,
184166
context
185167
)
168+
}.apply {
169+
val geoElement = popup.geoElement
170+
if (geoElement is DynamicEntity) {
171+
// For dynamic entities
172+
// update chart providers to use the new instances of PopupMedia to reacquire updated charts.
173+
updateMediaElement(element, scope)
174+
}
186175
}
187176
}
188177

@@ -195,7 +184,8 @@ internal fun rememberMediaElementState(
195184
* @property linkUrl the link to use to view the media for image type media in the media viewer
196185
* @property sourceUrl the link to use to render the image for image type media
197186
* @property type the type of the PopupMedia
198-
* @property scope a CoroutineScope to use to acquire chart images
187+
* @param uri the path to the media image omn disk, or empty if the image is not yet persisted.
188+
* @param scope a CoroutineScope to use to acquire chart images
199189
* @property imageGenerator a lambda which generates charts. Is only invoked if type is chart.
200190
*/
201191
internal class PopupMediaState(
@@ -206,7 +196,7 @@ internal class PopupMediaState(
206196
private val sourceUrl: String,
207197
val type: PopupMediaType,
208198
uri: String,
209-
private val scope: CoroutineScope,
199+
scope: CoroutineScope,
210200
private val imageGenerator: MediaImageProvider
211201
) {
212202
private val _imageUri: MutableState<String> = mutableStateOf("")
@@ -221,7 +211,7 @@ internal class PopupMediaState(
221211
_imageUri.value = uri
222212
} else {
223213
scope.launch {
224-
_imageUri.value = imageGenerator.get()
214+
_imageUri.value = imageGenerator.get("${UUID.randomUUID()}.png")
225215
}
226216
}
227217
}
@@ -242,6 +232,75 @@ internal class PopupMediaState(
242232
scope = scope,
243233
imageGenerator = imageGenerator
244234
)
235+
236+
internal fun updateMedia(media: PopupMedia, scope: CoroutineScope) {
237+
imageGenerator.media = media
238+
scope.launch {
239+
val oldMedia = File(_imageUri.value)
240+
val img = imageGenerator.get("${UUID.randomUUID()}.png")
241+
_imageUri.value = img
242+
if (oldMedia.exists()) {
243+
oldMedia.delete()
244+
}
245+
}
246+
}
247+
248+
companion object {
249+
internal fun createChartMediaState(
250+
media: PopupMedia,
251+
model: String,
252+
scope: CoroutineScope,
253+
imageFolder: String,
254+
chartParams: ChartImageParameters
255+
): PopupMediaState = PopupMediaState(
256+
media,
257+
scope,
258+
model,
259+
MediaImageProvider(
260+
folderName = imageFolder,
261+
media = media,
262+
) {
263+
it.generateChart(chartParams).getOrThrow().image.bitmap
264+
}
265+
)
266+
267+
internal fun createImageMediaState(
268+
media: PopupMedia,
269+
model: String,
270+
scope: CoroutineScope,
271+
imageFolder: String,
272+
context: Context
273+
): PopupMediaState {
274+
val srcUrl = media.value?.sourceUrl
275+
?: throw IllegalArgumentException("null sourceUrl for popup media")
276+
return PopupMediaState(
277+
media,
278+
scope,
279+
model,
280+
MediaImageProvider(
281+
folderName = imageFolder,
282+
media = media
283+
) {
284+
val request = ImageRequest.Builder(context)
285+
.data(srcUrl)
286+
.crossfade(true)
287+
.build()
288+
(context.imageLoader.execute(request).drawable as? BitmapDrawable)?.bitmap
289+
?: throw IllegalStateException("couldn't load image at $srcUrl")
290+
}
291+
).apply {
292+
if (refreshInterval > 0) {
293+
scope.launch {
294+
withContext(Dispatchers.IO) {
295+
// update the image according to the refresh interval
296+
delay(refreshInterval)
297+
updateMedia(media, scope)
298+
}
299+
}
300+
}
301+
}
302+
}
303+
}
245304
}
246305

247306
private val PopupMediaType.isChart: Boolean

0 commit comments

Comments
 (0)