diff options
5 files changed, 99 insertions, 67 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, + ) + } +} diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt index f43f1467..5d29b4f3 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt @@ -172,7 +172,7 @@ class CursorPreviewsInteractorTest { } ) .inOrder() - assertThat(startIdx).isEqualTo(0) + assertThat(startIdx).isEqualTo(2) assertThat(loadMoreLeft).isNull() assertThat(loadMoreRight).isNotNull() assertThat(leftTriggerIndex).isEqualTo(2) diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt index 09d254f3..0a56a2d0 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt @@ -167,7 +167,7 @@ class FetchPreviewsInteractorTest { with(cursorPreviewsRepository) { assertThat(previewsModel.value).isNotNull() - assertThat(previewsModel.value!!.startIdx).isEqualTo(0) + assertThat(previewsModel.value!!.startIdx).isEqualTo(2) assertThat(previewsModel.value!!.loadMoreLeft).isNull() assertThat(previewsModel.value!!.loadMoreRight).isNull() assertThat(previewsModel.value!!.previewModels) |