summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt5
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt151
3 files changed, 97 insertions, 65 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt
index 0e198f43..59e7e15e 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt
@@ -106,7 +106,7 @@ constructor(
val (leftTriggerIndex, rightTriggerIndex) = state.triggerIndices()
interactor.setPreviews(
previews = state.merged.values.toList(),
- startIndex = startPageNum,
+ startIndex = state.startIndex,
hasMoreLeft = state.hasMoreLeft,
hasMoreRight = state.hasMoreRight,
leftTriggerIndex = leftTriggerIndex,
@@ -144,7 +144,7 @@ constructor(
val loadingState: Flow<LoadDirection?> =
interactor.setPreviews(
previews = state.merged.values.toList(),
- startIndex = 0, // TODO: actually track this as the window changes?
+ startIndex = state.startIndex,
hasMoreLeft = state.hasMoreLeft,
hasMoreRight = state.hasMoreRight,
leftTriggerIndex = leftTriggerIndex,
@@ -215,6 +215,7 @@ constructor(
}
}
return CursorWindow(
+ startIndex = startPosition % pageSize,
firstLoadedPageNum = startPageIdx,
lastLoadedPageNum = startPageIdx,
pages = listOf(page.keys),
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt
index e2e69852..5e34b178 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt
@@ -18,6 +18,8 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.model
/** A window of data loaded from a cursor. */
data class LoadedWindow<K, V>(
+ /** The index position of the item that should be displayed initially. */
+ val startIndex: Int,
/** First cursor page index loaded within this window. */
val firstLoadedPageNum: Int,
/** Last cursor page index loaded within this window. */
@@ -42,6 +44,7 @@ fun <K, V> LoadedWindow<K, V>.shiftWindowRight(
hasMore: Boolean,
): LoadedWindow<K, V> =
LoadedWindow(
+ startIndex = startIndex - newPage.size,
firstLoadedPageNum = firstLoadedPageNum + 1,
lastLoadedPageNum = lastLoadedPageNum + 1,
pages = pages.drop(1) + listOf(newPage.keys),
@@ -61,6 +64,7 @@ fun <K, V> LoadedWindow<K, V>.expandWindowRight(
hasMore: Boolean,
): LoadedWindow<K, V> =
LoadedWindow(
+ startIndex = startIndex,
firstLoadedPageNum = firstLoadedPageNum,
lastLoadedPageNum = lastLoadedPageNum + 1,
pages = pages + listOf(newPage.keys),
@@ -75,6 +79,7 @@ fun <K, V> LoadedWindow<K, V>.shiftWindowLeft(
hasMore: Boolean,
): LoadedWindow<K, V> =
LoadedWindow(
+ startIndex = startIndex + newPage.size,
firstLoadedPageNum = firstLoadedPageNum - 1,
lastLoadedPageNum = lastLoadedPageNum - 1,
pages = listOf(newPage.keys) + pages.dropLast(1),
@@ -93,6 +98,7 @@ fun <K, V> LoadedWindow<K, V>.expandWindowLeft(
hasMore: Boolean,
): LoadedWindow<K, V> =
LoadedWindow(
+ startIndex = startIndex + newPage.size,
firstLoadedPageNum = firstLoadedPageNum - 1,
lastLoadedPageNum = lastLoadedPageNum,
pages = listOf(newPage.keys) + pages,
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 eab04aab..5b368084 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
@@ -57,11 +57,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layout
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.intentresolver.Flags.shareouselScrollOffscreenSelections
@@ -70,11 +73,13 @@ import com.android.intentresolver.R
import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.getOrDefault
import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselPreviewViewModel
import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
import kotlin.math.abs
import kotlin.math.min
+import kotlin.math.roundToInt
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@@ -106,52 +111,43 @@ private fun Shareousel(viewModel: ShareouselViewModel, keySet: PreviewsModel) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PreviewCarousel(previews: PreviewsModel, viewModel: ShareouselViewModel) {
- var maxAspectRatio by remember { mutableStateOf(0f) }
- var viewportHeight by remember { mutableStateOf(0) }
- var viewportCenter by remember { mutableStateOf(0) }
- var horizontalPadding by remember { mutableStateOf(0.dp) }
+ var measurements by remember { mutableStateOf(PreviewCarouselMeasurements.UNMEASURED) }
Box(
modifier =
Modifier.fillMaxWidth()
.height(dimensionResource(R.dimen.chooser_preview_image_height_tall))
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
- val (minItemWidth, maxAR) =
+ measurements =
if (placeable.height <= 0) {
- 0f to 0f
+ PreviewCarouselMeasurements.UNMEASURED
} else {
- val minItemWidth = (MIN_ASPECT_RATIO * placeable.height)
- val maxItemWidth = maxOf(0, placeable.width - 32.dp.roundToPx())
- val maxAR =
- (maxItemWidth.toFloat() / placeable.height).coerceIn(
- 0f,
- MAX_ASPECT_RATIO,
- )
- minItemWidth to maxAR
+ PreviewCarouselMeasurements(placeable, measureScope = this)
}
- viewportCenter = placeable.width / 2
- maxAspectRatio = maxAR
- viewportHeight = placeable.height
- horizontalPadding = ((placeable.width - minItemWidth) / 2).toDp()
layout(placeable.width, placeable.height) { placeable.place(0, 0) }
}
) {
- if (maxAspectRatio <= 0 && previews.previewModels.isNotEmpty()) {
- // Do not compose the list until we know the viewport size
- return@Box
- }
-
- var firstSelectedIndex by remember { mutableStateOf(null as Int?) }
+ // Do not compose the list until we have measured values
+ if (measurements == PreviewCarouselMeasurements.UNMEASURED) return@Box
val carouselState =
rememberLazyListState(
- prefetchStrategy = remember { ShareouselLazyListPrefetchStrategy() }
+ prefetchStrategy = remember { ShareouselLazyListPrefetchStrategy() },
+ initialFirstVisibleItemIndex = previews.startIdx,
+ initialFirstVisibleItemScrollOffset =
+ measurements.scrollOffsetToCenter(
+ previewModel = previews.previewModels[previews.startIdx]
+ ),
)
LazyRow(
state = carouselState,
horizontalArrangement = Arrangement.spacedBy(4.dp),
- contentPadding = PaddingValues(start = horizontalPadding, end = horizontalPadding),
+ contentPadding =
+ PaddingValues(
+ start = measurements.horizontalPaddingDp,
+ end = measurements.horizontalPaddingDp,
+ ),
modifier = Modifier.fillMaxSize().systemGestureExclusion(),
) {
itemsIndexed(
@@ -171,7 +167,7 @@ private fun PreviewCarousel(previews: PreviewsModel, viewModel: ShareouselViewMo
val halfPreviewWidth = it.size / 2
val previewCenter = it.offset + halfPreviewWidth
val previewDistanceToViewportCenter =
- abs(previewCenter - viewportCenter)
+ abs(previewCenter - measurements.viewportCenterPx)
if (previewDistanceToViewportCenter <= halfPreviewWidth) {
index
} else {
@@ -182,13 +178,12 @@ private fun PreviewCarousel(previews: PreviewsModel, viewModel: ShareouselViewMo
}
val previewModel =
- viewModel.preview(model, viewportHeight, previewIndex, rememberCoroutineScope())
- val selected by
- previewModel.isSelected.collectAsStateWithLifecycle(initialValue = false)
-
- if (selected) {
- firstSelectedIndex = min(index, firstSelectedIndex ?: Int.MAX_VALUE)
- }
+ viewModel.preview(
+ /* key = */ model,
+ /* previewHeight = */ measurements.viewportHeightPx,
+ /* index = */ previewIndex,
+ /* scope = */ rememberCoroutineScope(),
+ )
if (shareouselScrollOffscreenSelections()) {
LaunchedEffect(index, model.uri) {
@@ -209,10 +204,10 @@ private fun PreviewCarousel(previews: PreviewsModel, viewModel: ShareouselViewMo
when {
// Item is partially past start of viewport
item.offset < viewportStartOffset ->
- -viewportStartOffset
+ measurements.scrollOffsetToStartEdge()
// Item is partially past end of viewport
(item.offset + item.size) > viewportEndOffset ->
- item.size - viewportEndOffset
+ measurements.scrollOffsetToEndEdge(model)
// Item is fully within viewport
else -> null
}?.let { scrollOffset ->
@@ -230,29 +225,8 @@ private fun PreviewCarousel(previews: PreviewsModel, viewModel: ShareouselViewMo
}
ShareouselCard(
- viewModel.preview(
- model,
- viewportHeight,
- previewIndex,
- rememberCoroutineScope(),
- ),
- maxAspectRatio,
- )
- }
- }
-
- firstSelectedIndex?.let { index ->
- LaunchedEffect(Unit) {
- val visibleItem =
- carouselState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
- val center =
- with(carouselState.layoutInfo) {
- ((viewportEndOffset - viewportStartOffset) / 2) + viewportStartOffset
- }
-
- carouselState.scrollToItem(
- index = index,
- scrollOffset = visibleItem?.size?.div(2)?.minus(center) ?: 0,
+ viewModel = previewModel,
+ aspectRatio = measurements.coerceAspectRatio(previewModel.aspectRatio),
)
}
}
@@ -260,7 +234,7 @@ private fun PreviewCarousel(previews: PreviewsModel, viewModel: ShareouselViewMo
}
@Composable
-private fun ShareouselCard(viewModel: ShareouselPreviewViewModel, maxAspectRatio: Float) {
+private fun ShareouselCard(viewModel: ShareouselPreviewViewModel, aspectRatio: Float) {
val bitmapLoadState by viewModel.bitmapLoadState.collectAsStateWithLifecycle()
val selected by viewModel.isSelected.collectAsStateWithLifecycle(initialValue = false)
val borderColor = MaterialTheme.colorScheme.primary
@@ -281,7 +255,6 @@ private fun ShareouselCard(viewModel: ShareouselPreviewViewModel, maxAspectRatio
onValueChange = { scope.launch { viewModel.setSelected(it) } },
),
) { state ->
- val aspectRatio = minOf(maxAspectRatio, maxOf(MIN_ASPECT_RATIO, viewModel.aspectRatio))
if (state is ValueUpdate.Value) {
state.getOrDefault(null).let { bitmap ->
ShareouselCard(
@@ -398,5 +371,57 @@ private fun ShareouselAction(
inline fun Modifier.thenIf(condition: Boolean, crossinline factory: () -> Modifier): Modifier =
if (condition) this.then(factory()) else this
-private const val MIN_ASPECT_RATIO = 0.4f
-private const val MAX_ASPECT_RATIO = 2.5f
+private data class PreviewCarouselMeasurements(
+ val viewportHeightPx: Int,
+ val viewportWidthPx: Int,
+ val viewportCenterPx: Int = viewportWidthPx / 2,
+ val maxAspectRatio: Float,
+ val horizontalPaddingPx: Int,
+ val horizontalPaddingDp: Dp,
+) {
+ constructor(
+ placeable: Placeable,
+ measureScope: MeasureScope,
+ horizontalPadding: Float = (placeable.width - (MIN_ASPECT_RATIO * placeable.height)) / 2,
+ ) : this(
+ viewportHeightPx = placeable.height,
+ viewportWidthPx = placeable.width,
+ maxAspectRatio =
+ with(measureScope) {
+ min(
+ (placeable.width - 32.dp.roundToPx()).toFloat() / placeable.height,
+ MAX_ASPECT_RATIO,
+ )
+ },
+ horizontalPaddingPx = horizontalPadding.roundToInt(),
+ horizontalPaddingDp = with(measureScope) { horizontalPadding.toDp() },
+ )
+
+ fun coerceAspectRatio(ratio: Float): Float = ratio.coerceIn(MIN_ASPECT_RATIO, maxAspectRatio)
+
+ fun scrollOffsetToCenter(previewModel: PreviewModel): Int =
+ horizontalPaddingPx + (aspectRatioToWidthPx(previewModel.aspectRatio) / 2) -
+ viewportCenterPx
+
+ fun scrollOffsetToStartEdge(): Int = horizontalPaddingPx
+
+ fun scrollOffsetToEndEdge(previewModel: PreviewModel): Int =
+ horizontalPaddingPx + aspectRatioToWidthPx(previewModel.aspectRatio) - viewportWidthPx
+
+ private fun aspectRatioToWidthPx(ratio: Float): Int =
+ (coerceAspectRatio(ratio) * viewportHeightPx).roundToInt()
+
+ companion object {
+ private const val MIN_ASPECT_RATIO = 0.4f
+ private const val MAX_ASPECT_RATIO = 2.5f
+
+ val UNMEASURED =
+ PreviewCarouselMeasurements(
+ viewportHeightPx = 0,
+ viewportWidthPx = 0,
+ maxAspectRatio = 0f,
+ horizontalPaddingPx = 0,
+ horizontalPaddingDp = 0.dp,
+ )
+ }
+}