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

Commit 25e03a2c authored by Olivier St-Onge's avatar Olivier St-Onge
Browse files

Implement resizable tiles prototype

Tapping a tile in edit mode no longer adds or removes it. Instead, it selects the tile that was tapped.
A selected tile will show a colored border and resizing handle to the right.
Dragging that handle will resize the tile between 1x1 and 2x1.

Bug: 350984160
Test: manually
Test: ResizingStateTest
Test: MutableSelectionStateTest
Test: ResizingTest
Flag: com.android.systemui.qs_ui_refactor

Change-Id: Ic4cb3fbb0348aa53fa9e49f251046f151f92d9f1
parent ee2b5524
Loading
Loading
Loading
Loading
+131 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.qs.panels.ui.compose.selection

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class MutableSelectionStateTest : SysuiTestCase() {
    private val underTest = MutableSelectionState()

    @Test
    fun selectTile_isCorrectlySelected() {
        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()

        underTest.select(TEST_SPEC)
        assertThat(underTest.isSelected(TEST_SPEC)).isTrue()

        underTest.unSelect()
        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()

        val newSpec = TileSpec.create("newSpec")
        underTest.select(TEST_SPEC)
        underTest.select(newSpec)
        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
        assertThat(underTest.isSelected(newSpec)).isTrue()
    }

    @Test
    fun startResize_createsResizingState() {
        assertThat(underTest.resizingState).isNull()

        // Resizing starts but no tile is selected
        underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
        assertThat(underTest.resizingState).isNull()

        // Resizing starts with a selected tile
        underTest.select(TEST_SPEC)
        underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}

        assertThat(underTest.resizingState).isNotNull()
    }

    @Test
    fun endResize_clearsResizingState() {
        val spec = TileSpec.create("testSpec")

        // Resizing starts with a selected tile
        underTest.select(spec)
        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
        assertThat(underTest.resizingState).isNotNull()

        underTest.onResizingDragEnd()
        assertThat(underTest.resizingState).isNull()
    }

    @Test
    fun unselect_clearsResizingState() {
        // Resizing starts with a selected tile
        underTest.select(TEST_SPEC)
        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
        assertThat(underTest.resizingState).isNotNull()

        underTest.unSelect()
        assertThat(underTest.resizingState).isNull()
    }

    @Test
    fun onResizingDrag_updatesResizingState() {
        // Resizing starts with a selected tile
        underTest.select(TEST_SPEC)
        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
        assertThat(underTest.resizingState).isNotNull()

        underTest.onResizingDrag(5f)
        assertThat(underTest.resizingState?.width).isEqualTo(5)

        underTest.onResizingDrag(2f)
        assertThat(underTest.resizingState?.width).isEqualTo(7)

        underTest.onResizingDrag(-6f)
        assertThat(underTest.resizingState?.width).isEqualTo(1)
    }

    @Test
    fun onResizingDrag_receivesResizeCallback() {
        var resized = false
        val onResize: () -> Unit = { resized = !resized }

        // Resizing starts with a selected tile
        underTest.select(TEST_SPEC)
        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10), onResize)
        assertThat(underTest.resizingState).isNotNull()

        // Drag under the threshold
        underTest.onResizingDrag(1f)
        assertThat(resized).isFalse()

        // Drag over the threshold
        underTest.onResizingDrag(5f)
        assertThat(resized).isTrue()

        // Drag back under the threshold
        underTest.onResizingDrag(-5f)
        assertThat(resized).isFalse()
    }

    companion object {
        private val TEST_SPEC = TileSpec.create("testSpec")
    }
}
+62 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.qs.panels.ui.compose.selection

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class ResizingStateTest : SysuiTestCase() {

    @Test
    fun drag_updatesStateCorrectly() {
        var resized = false
        val underTest =
            ResizingState(TileWidths(base = 0, min = 0, max = 10)) { resized = !resized }

        assertThat(underTest.width).isEqualTo(0)

        underTest.onDrag(2f)
        assertThat(underTest.width).isEqualTo(2)

        underTest.onDrag(1f)
        assertThat(underTest.width).isEqualTo(3)
        assertThat(resized).isTrue()

        underTest.onDrag(-1f)
        assertThat(underTest.width).isEqualTo(2)
        assertThat(resized).isFalse()
    }

    @Test
    fun dragOutOfBounds_isClampedCorrectly() {
        val underTest = ResizingState(TileWidths(base = 0, min = 0, max = 10)) {}

        assertThat(underTest.width).isEqualTo(0)

        underTest.onDrag(100f)
        assertThat(underTest.width).isEqualTo(10)

        underTest.onDrag(-200f)
        assertThat(underTest.width).isEqualTo(0)
    }
}
+32 −29
Original line number Diff line number Diff line
@@ -17,9 +17,10 @@
package com.android.systemui.qs.panels.ui.compose

import android.content.ClipData
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
@@ -104,11 +105,10 @@ fun Modifier.dragAndDropRemoveZone(
@Composable
fun Modifier.dragAndDropTileList(
    gridState: LazyGridState,
    contentOffset: Offset,
    contentOffset: () -> Offset,
    dragAndDropState: DragAndDropState,
    onDrop: () -> Unit,
    onDrop: (TileSpec) -> Unit,
): Modifier {
    val currentContentOffset by rememberUpdatedState(contentOffset)
    val target =
        remember(dragAndDropState) {
            object : DragAndDropTarget {
@@ -118,7 +118,7 @@ fun Modifier.dragAndDropTileList(

                override fun onMoved(event: DragAndDropEvent) {
                    // Drag offset relative to the list's top left corner
                    val relativeDragOffset = event.dragOffsetRelativeTo(currentContentOffset)
                    val relativeDragOffset = event.dragOffsetRelativeTo(contentOffset())
                    val targetItem =
                        gridState.layoutInfo.visibleItemsInfo.firstOrNull { item ->
                            // Check if the drag is on this item
@@ -132,7 +132,7 @@ fun Modifier.dragAndDropTileList(

                override fun onDrop(event: DragAndDropEvent): Boolean {
                    return dragAndDropState.draggedCell?.let {
                        onDrop()
                        onDrop(it.tile.tileSpec)
                        dragAndDropState.onDrop()
                        true
                    } ?: false
@@ -158,24 +158,26 @@ private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean {
    return item.span != 1 && offset.x > itemCenter.x
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Modifier.dragAndDropTileSource(
    sizedTile: SizedTile<EditTileViewModel>,
    dragAndDropState: DragAndDropState,
    onTap: (TileSpec) -> Unit,
    onDoubleTap: (TileSpec) -> Unit = {},
    onDragStart: () -> Unit,
): Modifier {
    val state by rememberUpdatedState(dragAndDropState)
    return dragAndDropSource {
        detectTapGestures(
            onTap = { onTap(sizedTile.tile.tileSpec) },
            onDoubleTap = { onDoubleTap(sizedTile.tile.tileSpec) },
            onLongPress = {
                state.onStarted(sizedTile)

                // The tilespec from the ClipData transferred isn't actually needed as we're moving
                // a tile within the same application. We're using a custom MIME type to limit the
                // drag event to QS.
    val dragState by rememberUpdatedState(dragAndDropState)
    @Suppress("DEPRECATION") // b/368361871
    return dragAndDropSource(
        block = {
            detectDragGesturesAfterLongPress(
                onDrag = { _, _ -> },
                onDragStart = {
                    dragState.onStarted(sizedTile)
                    onDragStart()

                    // The tilespec from the ClipData transferred isn't actually needed as we're
                    // moving a tile within the same application. We're using a custom MIME type to
                    // limit the drag event to QS.
                    startTransfer(
                        DragAndDropTransferData(
                            ClipData(
@@ -188,6 +190,7 @@ fun Modifier.dragAndDropTileSource(
                },
            )
        }
    )
}

private object QsDragAndDrop {
+4 −5
Original line number Diff line number Diff line
@@ -42,10 +42,8 @@ fun rememberEditListState(
}

/** Holds the temporary state of the tile list during a drag movement where we move tiles around. */
class EditTileListState(
    tiles: List<SizedTile<EditTileViewModel>>,
    private val columns: Int,
) : DragAndDropState {
class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val columns: Int) :
    DragAndDropState {
    private val _draggedCell = mutableStateOf<SizedTile<EditTileViewModel>?>(null)
    override val draggedCell
        get() = _draggedCell.value
@@ -91,7 +89,8 @@ class EditTileListState(
            regenerateGrid(includeSpacers = true)
            _tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell)
        } else {
            // Add the tile with a temporary row which will get reassigned when regenerating spacers
            // Add the tile with a temporary row which will get reassigned when
            // regenerating spacers
            _tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0))
        }

+3 −2
Original line number Diff line number Diff line
@@ -127,13 +127,14 @@ fun LargeTileContent(
}

@Composable
private fun LargeTileLabels(
fun LargeTileLabels(
    label: String,
    secondaryLabel: String?,
    colors: TileColors,
    modifier: Modifier = Modifier,
    accessibilityUiState: AccessibilityUiState? = null,
) {
    Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
    Column(verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxHeight()) {
        Text(label, color = colors.label, modifier = Modifier.tileMarquee())
        if (!TextUtils.isEmpty(secondaryLabel)) {
            Text(
Loading