Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e7e595de authored by Lucas Silva's avatar Lucas Silva
Browse files

Address some issues with ResizeableItemFrame

1. Use rememberUpdatedState to remember the resize callback, and ensure
   we always use the latest one in the event of recomposition.
2. Make sure we clear anchors when the layout info becomes unknown, such
   as when the item is scrolled out-of-view.
3. Instead of using the index, use a key to locate items. The index may
   change if the item is moved as part of resizing, which will cause a
   recomposition. However, the key should remain stable.

Bug: 368056517
Test: atest ResizeableItemFrameViewModelTest
Flag: EXEMPT component is not yet used anywhere
Change-Id: I17d4fc1180866d18bfb61f8de739e794e80da886
parent 4c3a7c43
Loading
Loading
Loading
Loading
+14 −11
Original line number Original line Diff line number Diff line
@@ -30,6 +30,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
@@ -52,12 +54,11 @@ import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.lifecycle.rememberViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull


@Composable
@Composable
private fun UpdateGridLayoutInfo(
private fun UpdateGridLayoutInfo(
    viewModel: ResizeableItemFrameViewModel,
    viewModel: ResizeableItemFrameViewModel,
    index: Int,
    key: String,
    gridState: LazyGridState,
    gridState: LazyGridState,
    minItemSpan: Int,
    minItemSpan: Int,
    gridContentPadding: PaddingValues,
    gridContentPadding: PaddingValues,
@@ -67,7 +68,7 @@ private fun UpdateGridLayoutInfo(
    LaunchedEffect(
    LaunchedEffect(
        density,
        density,
        viewModel,
        viewModel,
        index,
        key,
        gridState,
        gridState,
        minItemSpan,
        minItemSpan,
        gridContentPadding,
        gridContentPadding,
@@ -85,9 +86,8 @@ private fun UpdateGridLayoutInfo(
                snapshotFlow { gridState.layoutInfo.maxSpan },
                snapshotFlow { gridState.layoutInfo.maxSpan },
                snapshotFlow { gridState.layoutInfo.viewportSize.height },
                snapshotFlow { gridState.layoutInfo.viewportSize.height },
                snapshotFlow {
                snapshotFlow {
                        gridState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
                    gridState.layoutInfo.visibleItemsInfo.firstOrNull { it.key == key }
                    }
                },
                    .filterNotNull(),
                ::Triple,
                ::Triple,
            )
            )
            .collectLatest { (maxItemSpan, viewportHeightPx, itemInfo) ->
            .collectLatest { (maxItemSpan, viewportHeightPx, itemInfo) ->
@@ -97,8 +97,8 @@ private fun UpdateGridLayoutInfo(
                    viewportHeightPx,
                    viewportHeightPx,
                    maxItemSpan,
                    maxItemSpan,
                    minItemSpan,
                    minItemSpan,
                    itemInfo.row,
                    itemInfo?.row,
                    itemInfo.span,
                    itemInfo?.span,
                )
                )
            }
            }
    }
    }
@@ -161,7 +161,7 @@ private fun BoxScope.DragHandle(
 */
 */
@Composable
@Composable
fun ResizableItemFrame(
fun ResizableItemFrame(
    index: Int,
    key: String,
    gridState: LazyGridState,
    gridState: LazyGridState,
    minItemSpan: Int,
    minItemSpan: Int,
    gridContentPadding: PaddingValues,
    gridContentPadding: PaddingValues,
@@ -177,6 +177,7 @@ fun ResizableItemFrame(
    content: @Composable () -> Unit,
    content: @Composable () -> Unit,
) {
) {
    val brush = SolidColor(outlineColor)
    val brush = SolidColor(outlineColor)
    val onResizeUpdated by rememberUpdatedState(onResize)
    val viewModel =
    val viewModel =
        rememberViewModel(traceName = "ResizeableItemFrame.viewModel") {
        rememberViewModel(traceName = "ResizeableItemFrame.viewModel") {
            ResizeableItemFrameViewModel()
            ResizeableItemFrameViewModel()
@@ -230,13 +231,15 @@ fun ResizableItemFrame(


            UpdateGridLayoutInfo(
            UpdateGridLayoutInfo(
                viewModel,
                viewModel,
                index,
                key,
                gridState,
                gridState,
                minItemSpan,
                minItemSpan,
                gridContentPadding,
                gridContentPadding,
                verticalArrangement,
                verticalArrangement,
            )
            )
            LaunchedEffect(viewModel) { viewModel.resizeInfo.collectLatest(onResize) }
            LaunchedEffect(viewModel) {
                viewModel.resizeInfo.collectLatest { info -> onResizeUpdated(info) }
            }
        }
        }
    }
    }
}
}
+19 −2
Original line number Original line Diff line number Diff line
@@ -254,6 +254,23 @@ class ResizeableItemFrameViewModelTest : SysuiTestCase() {
        assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM))
        assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM))
    }
    }


    @Test
    fun testRowInfoBecomesNull_revertsBackToDefault() =
        testScope.runTest {
            val gridLayout = singleSpanGrid.copy(maxItemSpan = 3, currentRow = 1)
            updateGridLayout(gridLayout)

            val topState = underTest.topDragState
            assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -30f)

            val bottomState = underTest.bottomDragState
            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 30f)

            updateGridLayout(gridLayout.copy(currentRow = null))
            assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
        }

    @Test(expected = IllegalArgumentException::class)
    @Test(expected = IllegalArgumentException::class)
    fun testIllegalState_maxSpanSmallerThanMinSpan() =
    fun testIllegalState_maxSpanSmallerThanMinSpan() =
        testScope.runTest {
        testScope.runTest {
@@ -317,7 +334,7 @@ class ResizeableItemFrameViewModelTest : SysuiTestCase() {
        val viewportHeightPx: Int,
        val viewportHeightPx: Int,
        val maxItemSpan: Int,
        val maxItemSpan: Int,
        val minItemSpan: Int,
        val minItemSpan: Int,
        val currentRow: Int,
        val currentRow: Int?,
        val currentSpan: Int,
        val currentSpan: Int?,
    )
    )
}
}
+14 −9
Original line number Original line Diff line number Diff line
@@ -25,8 +25,7 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.merge
@@ -45,7 +44,10 @@ data class ResizeInfo(
    val spans: Int,
    val spans: Int,
    /** The drag handle which was used to resize the element. */
    /** The drag handle which was used to resize the element. */
    val fromHandle: DragHandle,
    val fromHandle: DragHandle,
)
) {
    /** Whether we are expanding. If false, then we are shrinking. */
    val isExpanding = spans > 0
}


class ResizeableItemFrameViewModel : ExclusiveActivatable() {
class ResizeableItemFrameViewModel : ExclusiveActivatable() {
    private data class GridLayoutInfo(
    private data class GridLayoutInfo(
@@ -73,7 +75,7 @@ class ResizeableItemFrameViewModel : ExclusiveActivatable() {
                snapshotFlow { bottomDragState.settledValue }
                snapshotFlow { bottomDragState.settledValue }
                    .map { ResizeInfo(it, DragHandle.BOTTOM) },
                    .map { ResizeInfo(it, DragHandle.BOTTOM) },
            )
            )
            .dropWhile { it.spans == 0 }
            .filter { it.spans != 0 }
            .distinctUntilChanged()
            .distinctUntilChanged()


    /**
    /**
@@ -86,9 +88,13 @@ class ResizeableItemFrameViewModel : ExclusiveActivatable() {
        viewportHeightPx: Int,
        viewportHeightPx: Int,
        maxItemSpan: Int,
        maxItemSpan: Int,
        minItemSpan: Int,
        minItemSpan: Int,
        currentRow: Int,
        currentRow: Int?,
        currentSpan: Int,
        currentSpan: Int?,
    ) {
    ) {
        if (currentSpan == null || currentRow == null) {
            gridLayoutInfo.value = null
            return
        }
        require(maxItemSpan >= minItemSpan) {
        require(maxItemSpan >= minItemSpan) {
            "Maximum item span of $maxItemSpan cannot be less than the minimum span of $minItemSpan"
            "Maximum item span of $maxItemSpan cannot be less than the minimum span of $minItemSpan"
        }
        }
@@ -114,10 +120,10 @@ class ResizeableItemFrameViewModel : ExclusiveActivatable() {


    private fun calculateAnchorsForHandle(
    private fun calculateAnchorsForHandle(
        handle: DragHandle,
        handle: DragHandle,
        layoutInfo: GridLayoutInfo,
        layoutInfo: GridLayoutInfo?,
    ): DraggableAnchors<Int> {
    ): DraggableAnchors<Int> {


        if (!isDragAllowed(handle, layoutInfo)) {
        if (layoutInfo == null || !isDragAllowed(handle, layoutInfo)) {
            return DraggableAnchors { 0 at 0f }
            return DraggableAnchors { 0 at 0f }
        }
        }


@@ -188,7 +194,6 @@ class ResizeableItemFrameViewModel : ExclusiveActivatable() {
    override suspend fun onActivated(): Nothing {
    override suspend fun onActivated(): Nothing {
        coroutineScope("ResizeableItemFrameViewModel.onActivated") {
        coroutineScope("ResizeableItemFrameViewModel.onActivated") {
            gridLayoutInfo
            gridLayoutInfo
                .filterNotNull()
                .onEach { layoutInfo ->
                .onEach { layoutInfo ->
                    topDragState.updateAnchors(
                    topDragState.updateAnchors(
                        calculateAnchorsForHandle(DragHandle.TOP, layoutInfo)
                        calculateAnchorsForHandle(DragHandle.TOP, layoutInfo)