summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
author Matt Casey <mrcasey@google.com> 2024-06-05 19:54:53 +0000
committer Matt Casey <mrcasey@google.com> 2024-06-06 18:08:21 +0000
commit02c6c6bd7d820a45b3d13104a41ef19673881a71 (patch)
tree6370859bd3b91275ec7aedd2e0faa99e997353f8 /java/src
parent466184452d186a4838564d97f518ac4c2fa9288a (diff)
Fade in shareousel items as they load
Switched to a StateFlow with the intiial cached value when it exists so that we don't fade in when we immediately have the bitmap. Bug: 344961968 Test: atest ShareoulselViewModelTest Test: Manual test with ShareTest and slow fade times. Flag: android.service.chooser.chooser_payload_toggling Change-Id: I14c9c82343e7e8a9330695121eebe17c65f1dcb9
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt5
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageLoader.kt3
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt33
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt85
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt3
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt20
6 files changed, 85 insertions, 64 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt
index ce064cdf..2e2aa938 100644
--- a/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt
+++ b/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt
@@ -28,6 +28,7 @@ import javax.inject.Qualifier
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
@@ -104,6 +105,10 @@ constructor(
// [CancellationExceptions]s so that they don't cancel the calling coroutine/scope.
runCatching { cache[uri].await() }.getOrNull()
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun getCachedBitmap(uri: Uri): Bitmap? =
+ kotlin.runCatching { cache[uri].getCompleted() }.getOrNull()
+
companion object {
private const val TAG = "CachingImgPrevLoader"
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
index 629651a3..81913a8e 100644
--- a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
+++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
@@ -35,6 +35,9 @@ interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitm
/** Prepopulate the image loader cache. */
fun prePopulate(uris: List<Uri>)
+ /** Returns a bitmap for the given URI if it's already cached, otherwise null */
+ fun getCachedBitmap(uri: Uri): Bitmap? = null
+
/** Load preview image; caching is allowed. */
override suspend fun invoke(uri: Uri) = invoke(uri, true)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt
index 71d16da9..197d6858 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt
@@ -40,28 +40,25 @@ fun ShareouselCard(
image: @Composable () -> Unit,
contentType: ContentType,
selected: Boolean,
- loadComplete: Boolean,
modifier: Modifier = Modifier,
) {
Box(modifier) {
image()
- if (loadComplete) {
- val topButtonPadding = 12.dp
- Box(modifier = Modifier.padding(topButtonPadding).matchParentSize()) {
- SelectionIcon(selected, modifier = Modifier.align(Alignment.TopStart))
- when (contentType) {
- ContentType.Video ->
- TypeIcon(
- R.drawable.ic_play_circle_filled_24px,
- modifier = Modifier.align(Alignment.TopEnd)
- )
- ContentType.Other ->
- TypeIcon(
- R.drawable.chooser_file_generic,
- modifier = Modifier.align(Alignment.TopEnd)
- )
- ContentType.Image -> Unit // No additional icon needed.
- }
+ val topButtonPadding = 12.dp
+ Box(modifier = Modifier.padding(topButtonPadding).matchParentSize()) {
+ SelectionIcon(selected, modifier = Modifier.align(Alignment.TopStart))
+ when (contentType) {
+ ContentType.Video ->
+ TypeIcon(
+ R.drawable.ic_play_circle_filled_24px,
+ modifier = Modifier.align(Alignment.TopEnd)
+ )
+ ContentType.Other ->
+ TypeIcon(
+ R.drawable.chooser_file_generic,
+ modifier = Modifier.align(Alignment.TopEnd)
+ )
+ ContentType.Image -> Unit // No additional icon needed.
}
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt
index a40a9c50..c63055d2 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt
@@ -15,6 +15,7 @@
*/
package com.android.intentresolver.contentpreview.payloadtoggle.ui.composable
+import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -126,15 +127,14 @@ private fun PreviewCarousel(
}
}
- ShareouselCard(viewModel.preview(model, previewIndex))
+ ShareouselCard(viewModel.preview(model, previewIndex, rememberCoroutineScope()))
}
}
}
@Composable
private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) {
- val bitmapLoadState by
- viewModel.bitmapLoadState.collectAsStateWithLifecycle(initialValue = ValueUpdate.Absent)
+ val bitmapLoadState by viewModel.bitmapLoadState.collectAsStateWithLifecycle()
val selected by viewModel.isSelected.collectAsStateWithLifecycle(initialValue = false)
val borderColor = MaterialTheme.colorScheme.primary
val scope = rememberCoroutineScope()
@@ -144,47 +144,56 @@ private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) {
ContentType.Video -> stringResource(R.string.selectable_video)
else -> stringResource(R.string.selectable_item)
}
- // Image load is complete (but may have failed)
- val loadComplete = bitmapLoadState is ValueUpdate.Value
- ShareouselCard(
- image = {
- // TODO: max ratio is actually equal to the viewport ratio
- val aspectRatio = viewModel.aspectRatio.coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO)
- bitmapLoadState.getOrDefault(null)?.let { bitmap ->
- Image(
- bitmap = bitmap.asImageBitmap(),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier = Modifier.aspectRatio(aspectRatio),
- )
- }
- ?: run {
- // TODO: look at ScrollableImagePreviewView.setLoading()
- Box(
- modifier =
- Modifier.fillMaxHeight()
- .aspectRatio(aspectRatio)
- .background(color = MaterialTheme.colorScheme.surfaceContainerHigh)
- )
- }
- },
- contentType = viewModel.contentType,
- loadComplete = loadComplete,
- selected = selected,
+ Crossfade(
+ targetState = bitmapLoadState,
modifier =
- Modifier.thenIf(selected && loadComplete) {
- Modifier.border(
- width = 4.dp,
- color = borderColor,
- shape = RoundedCornerShape(size = 12.dp),
- )
- }
- .semantics { this.contentDescription = contentDescription }
+ Modifier.semantics { this.contentDescription = contentDescription }
.clip(RoundedCornerShape(size = 12.dp))
.toggleable(
value = selected,
onValueChange = { scope.launch { viewModel.setSelected(it) } },
)
+ ) { state ->
+ // TODO: max ratio is actually equal to the viewport ratio
+ val aspectRatio = viewModel.aspectRatio.coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO)
+ if (state is ValueUpdate.Value) {
+ state.getOrDefault(null).let { bitmap ->
+ ShareouselCard(
+ image = {
+ bitmap?.let {
+ Image(
+ bitmap = bitmap.asImageBitmap(),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.aspectRatio(aspectRatio),
+ )
+ } ?: PlaceholderBox(aspectRatio)
+ },
+ contentType = viewModel.contentType,
+ selected = selected,
+ modifier =
+ Modifier.thenIf(selected) {
+ Modifier.border(
+ width = 4.dp,
+ color = borderColor,
+ shape = RoundedCornerShape(size = 12.dp),
+ )
+ }
+ )
+ }
+ } else {
+ PlaceholderBox(aspectRatio)
+ }
+ }
+}
+
+@Composable
+private fun PlaceholderBox(aspectRatio: Float) {
+ Box(
+ modifier =
+ Modifier.fillMaxHeight()
+ .aspectRatio(aspectRatio)
+ .background(color = MaterialTheme.colorScheme.surfaceContainerHigh)
)
}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt
index 1acdcf7a..de435290 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt
@@ -20,11 +20,12 @@ import android.graphics.Bitmap
import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
/** An individual preview within Shareousel. */
data class ShareouselPreviewViewModel(
/** Image to be shared. */
- val bitmapLoadState: Flow<ValueUpdate<Bitmap?>>,
+ val bitmapLoadState: StateFlow<ValueUpdate<Bitmap?>>,
/** Type of data to be shared. */
val contentType: ContentType,
/** Whether this preview has been selected by the user. */
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt
index 19acb318..d0b89860 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt
@@ -56,7 +56,8 @@ data class ShareouselViewModel(
/** List of action chips presented underneath Shareousel. */
val actions: Flow<List<ActionChipViewModel>>,
/** Creates a [ShareouselPreviewViewModel] for a [PreviewModel] present in [previews]. */
- val preview: (key: PreviewModel, index: Int?) -> ShareouselPreviewViewModel,
+ val preview:
+ (key: PreviewModel, index: Int?, scope: CoroutineScope) -> ShareouselPreviewViewModel,
)
@Module
@@ -113,7 +114,7 @@ interface ShareouselViewModelModule {
}
}
},
- preview = { key, index ->
+ preview = { key, index, previewScope ->
keySet.value?.maybeLoad(index)
val previewInteractor = interactor.preview(key)
val contentType =
@@ -122,14 +123,19 @@ interface ShareouselViewModelModule {
mimeTypeClassifier.isVideoType(key.mimeType) -> ContentType.Video
else -> ContentType.Other
}
+ val initialBitmapValue =
+ key.previewUri?.let {
+ imageLoader.getCachedBitmap(it)?.let { ValueUpdate.Value(it) }
+ } ?: ValueUpdate.Absent
ShareouselPreviewViewModel(
bitmapLoadState =
flow {
- emit(
- key.previewUri?.let { ValueUpdate.Value(imageLoader(it)) }
- ?: ValueUpdate.Absent
- )
- },
+ emit(
+ key.previewUri?.let { ValueUpdate.Value(imageLoader(it)) }
+ ?: ValueUpdate.Absent
+ )
+ }
+ .stateIn(previewScope, SharingStarted.Eagerly, initialBitmapValue),
contentType = contentType,
isSelected = previewInteractor.isSelected,
setSelected = previewInteractor::setSelected,