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

Commit 3070277b authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "System UI: allow users to draw the rectangle in region select" into main

parents 32fc8183 a0541438
Loading
Loading
Loading
Loading
+2 −7
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@


package com.android.systemui.screencapture.record.largescreen.ui.compose
package com.android.systemui.screencapture.record.largescreen.ui.compose


import android.graphics.Rect
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxWidth
@@ -24,9 +25,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.compose.ui.zIndex
import com.android.systemui.res.R
import com.android.systemui.res.R
@@ -74,11 +73,7 @@ fun PreCaptureUI(viewModel: PreCaptureViewModel) {
                // TODO(b/427541309) Set the initial width and height of the RegionBox based on the
                // TODO(b/427541309) Set the initial width and height of the RegionBox based on the
                // viewmodel state.
                // viewmodel state.
                RegionBox(
                RegionBox(
                    initialWidth = 100.dp,
                    onRegionSelected = { rect: Rect -> viewModel.updateRegionBox(rect) },
                    initialHeight = 100.dp,
                    onDragEnd = { _: Offset, _: Dp, _: Dp ->
                        // TODO(b/427541309) Update the region box in the viewmodel.
                    },
                    drawableLoaderViewModel = viewModel,
                    drawableLoaderViewModel = viewModel,
                )
                )
            }
            }
+197 −135
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@


package com.android.systemui.screencapture.record.largescreen.ui.compose
package com.android.systemui.screencapture.record.largescreen.ui.compose


import android.graphics.Rect as IntRect
import androidx.compose.foundation.border
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Box
@@ -31,18 +32,29 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import com.android.systemui.res.R
import com.android.systemui.res.R
import com.android.systemui.screencapture.common.ui.compose.PrimaryButton
import com.android.systemui.screencapture.common.ui.compose.PrimaryButton
import com.android.systemui.screencapture.common.ui.compose.loadIcon
import com.android.systemui.screencapture.common.ui.compose.loadIcon
import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModel
import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModel
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt

// The different modes of interaction that the user can have with the RegionBox.
private enum class DragMode {
    DRAWING,
    MOVING,
    RESIZING,
    NONE,
}


/**
/**
 * Determines which zone (corner or edge) of a box is being touched based on the press offset.
 * Determines which zone (corner or edge) of a box is being touched based on the press offset.
@@ -59,6 +71,18 @@ private fun getTouchedZone(
    startOffset: Offset,
    startOffset: Offset,
    touchAreaPx: Float,
    touchAreaPx: Float,
): ResizeZone? {
): ResizeZone? {
    // Check if the touch is within the touch area of the box.
    val touchedZone =
        Rect(
            left = -touchAreaPx,
            top = -touchAreaPx,
            right = boxWidth + touchAreaPx,
            bottom = boxHeight + touchAreaPx,
        )
    if (!touchedZone.contains(startOffset)) {
        return null
    }

    val isTouchingTop = startOffset.y in -touchAreaPx..touchAreaPx
    val isTouchingTop = startOffset.y in -touchAreaPx..touchAreaPx
    val isTouchingBottom = startOffset.y in (boxHeight - touchAreaPx)..(boxHeight + touchAreaPx)
    val isTouchingBottom = startOffset.y in (boxHeight - touchAreaPx)..(boxHeight + touchAreaPx)
    val isTouchingLeft = startOffset.x in -touchAreaPx..touchAreaPx
    val isTouchingLeft = startOffset.x in -touchAreaPx..touchAreaPx
@@ -82,107 +106,146 @@ private fun getTouchedZone(
}
}


/**
/**
 * A stateful composable that manages the size and position of a resizable RegionBox.
 * A class that encapsulates the state and logic for the RegionBox composable.
 *
 *
 * @param initialWidth The initial width of the box.
 * @param minSizePx The minimum size of the box in pixels.
 * @param initialHeight The initial height of the box.
 * @param touchAreaPx The size of the touch area for resizing in pixels.
 * @param onDragEnd A callback function that is invoked with the final offset, width, and height
 *   when the user finishes a drag gesture.
 * @param initialOffset The initial top-left offset of the box. Default is (0, 0), which is the
 *   parent's top-left corner.
 * @param modifier The modifier to be applied to the composable.
 */
 */
@Composable
private class RegionBoxState(private val minSizePx: Float, private val touchAreaPx: Float) {
fun RegionBox(
    var rect by mutableStateOf<Rect?>(null)
    initialWidth: Dp,
        private set
    initialHeight: Dp,
    onDragEnd: (offset: Offset, width: Dp, height: Dp) -> Unit,
    drawableLoaderViewModel: DrawableLoaderViewModel,
    initialOffset: Offset = Offset.Zero,
    modifier: Modifier = Modifier,
) {
    // The minimum size allowed for the box.
    val minSize = 1.dp


    val density = LocalDensity.current
    private var dragMode by mutableStateOf(DragMode.NONE)
    val minSizePx = remember(density) { with(density) { minSize.toPx() } }
    private var resizeZone by mutableStateOf<ResizeZone?>(null)


    // State for the region box's geometry.
    // The offset of the initial press when the user starts a drag gesture.
    var rect by remember {
    private var newBoxStartOffset by mutableStateOf(Offset.Zero)
        mutableStateOf(

            with(density) {
    // Must remember the screen size for the drag logic. Initial values are set to 0.
                // offset is how far from the parent's top-left corner the box should be placed.
    var screenWidth by mutableStateOf(0f)
                Rect(offset = initialOffset, size = Size(initialWidth.toPx(), initialHeight.toPx()))
    var screenHeight by mutableStateOf(0f)
            }

    fun startDrag(startOffset: Offset) {
        val currentRect = rect

        if (currentRect == null) {
            // If the box is not yet created, it is a drawing drag.
            dragMode = DragMode.DRAWING
            newBoxStartOffset = startOffset
        } else {
            // The offset of the existing box.
            val currentRectOffset = startOffset - currentRect.topLeft
            val touchedZone =
                getTouchedZone(
                    currentRect.width,
                    currentRect.height,
                    currentRectOffset,
                    touchAreaPx,
                )
                )
            when {
                touchedZone != null -> {
                    // If the drag was initiated within the current rectangle's drag-to-resize touch
                    // zone, it is a resizing drag.
                    dragMode = DragMode.RESIZING
                    resizeZone = touchedZone
                }
                currentRect.contains(startOffset) -> {
                    // If the drag was initiated inside the rectangle and not within the touch
                    // zones, it is a moving drag.
                    dragMode = DragMode.MOVING
                }
                else -> {
                    // The touch was initiated outside of the rectangle and its touch zone.
                    dragMode = DragMode.DRAWING
                    newBoxStartOffset = startOffset
                }
            }
        }
    }
    }


    val onBoxDrag: (dragAmount: Offset, maxWidth: Float, maxHeight: Float) -> Unit =
    fun drag(endOffset: Offset, dragAmount: Offset) {
        { dragAmount, maxWidth, maxHeight ->
        val currentRect = rect
            val newOffset = rect.topLeft + dragAmount
        when (dragMode) {
            DragMode.DRAWING -> {
                // Ensure that the box remains within the boundaries of the screen.
                val newBoxEndOffset =
                    Offset(
                        x = endOffset.x.coerceIn(0f, screenWidth),
                        y = endOffset.y.coerceIn(0f, screenHeight),
                    )
                rect =
                    Rect(
                        left = min(newBoxStartOffset.x, newBoxEndOffset.x),
                        top = min(newBoxStartOffset.y, newBoxEndOffset.y),
                        right = max(newBoxStartOffset.x, newBoxEndOffset.x),
                        bottom = max(newBoxStartOffset.y, newBoxEndOffset.y),
                    )
            }
            DragMode.MOVING -> {
                if (currentRect != null) {
                    val newOffset = currentRect.topLeft + dragAmount


                    // Constrain the new position within the parent's boundaries
                    // Constrain the new position within the parent's boundaries
            val constrainedLeft: Float = newOffset.x.coerceIn(0f, maxWidth - rect.width)
                    val constrainedLeft = newOffset.x.coerceIn(0f, screenWidth - currentRect.width)
            val constrainedTop: Float = newOffset.y.coerceIn(0f, maxHeight - rect.height)
                    val constrainedTop = newOffset.y.coerceIn(0f, screenHeight - currentRect.height)


                    rect =
                    rect =
                rect.translate(
                        currentRect.translate(
                    translateX = constrainedLeft - rect.left,
                            translateX = constrainedLeft - currentRect.left,
                    translateY = constrainedTop - rect.top,
                            translateY = constrainedTop - currentRect.top,
                        )
                        )
                }
                }

            }
    ResizableRectangle(
            DragMode.RESIZING -> {
        rect = rect,
                if (currentRect != null && resizeZone != null) {
        onResizeDrag = { dragAmount, zone, maxWidth, maxHeight ->
                    rect =
            rect = zone.processResizeDrag(rect, dragAmount, minSizePx, maxWidth, maxHeight)
                        resizeZone!!.processResizeDrag(
        },
                            currentRect,
        onBoxDrag = onBoxDrag,
                            dragAmount,
        onDragEnd = {
                            minSizePx,
            onDragEnd(
                            screenWidth,
                Offset(rect.left, rect.top),
                            screenHeight,
                with(density) { rect.width.toDp() },
                with(density) { rect.height.toDp() },
            )
        },
        drawableLoaderViewModel = drawableLoaderViewModel,
        modifier = modifier,
                        )
                        )
                }
                }
            }
            DragMode.NONE -> {
                // Do nothing.
            }
        }
    }

    fun dragEnd() {
        dragMode = DragMode.NONE
        resizeZone = null
    }
}


/**
/**
 * A box with a border that can be resized by dragging its zone (corner or edge), and moved by
 * A composable that allows the user to create, move, resize, and redraw a rectangular region.
 * dragging its body.
 *
 *
 * @param rect The current geometry of the region box.
 * @param onRegionSelected A callback function that is invoked with the final rectangle when the
 * @param onResizeDrag Callback invoked when a corner or edge is dragged.
 *   user finishes a drag gesture. This rectangle is used for taking a screenshot. The rectangle is
 * @param onBoxDrag Callback invoked when the main body of the box is dragged.
 *   of type [android.graphics.Rect] because the screenshot API requires int values.
 * @param onDragEnd Callback invoked when a drag gesture finishes.
 * @param drawableLoaderViewModel The view model that is used to load drawables.
 * @param modifier The modifier to be applied to the composable.
 * @param modifier The modifier to be applied to the composable.
 */
 */
@Composable
@Composable
private fun ResizableRectangle(
fun RegionBox(
    rect: Rect,
    onRegionSelected: (rect: IntRect) -> Unit,
    onResizeDrag: (dragAmount: Offset, zone: ResizeZone, maxWidth: Float, maxHeight: Float) -> Unit,
    onBoxDrag: (dragAmount: Offset, maxWidth: Float, maxHeight: Float) -> Unit,
    onDragEnd: () -> Unit,
    drawableLoaderViewModel: DrawableLoaderViewModel,
    drawableLoaderViewModel: DrawableLoaderViewModel,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
) {
) {
    // The width of the border stroke around the region box.
    val density = LocalDensity.current
    val borderStrokeWidth = 4.dp
    // The touch area for detecting an edge or corner resize drag.
    val touchArea = 48.dp


    // Must remember the screen size for the drag logic. Initial values are set to 0.
    // The minimum size allowed for the box.
    var screenWidth by remember { mutableStateOf(0f) }
    val minSize = 1.dp
    var screenHeight by remember { mutableStateOf(0f) }
    val minSizePx = remember(density) { with(density) { minSize.toPx() } }


    val density = LocalDensity.current
    // The touch area for detecting an edge or corner resize drag.
    val touchAreaPx = with(density) { touchArea.toPx() }
    val touchArea = 48.dp
    val touchAreaPx = remember(density) { with(density) { touchArea.toPx() } }


    // The zone being dragged for resizing, if any.
    val state = remember { RegionBoxState(minSizePx, touchAreaPx) }
    var draggedZone by remember { mutableStateOf<ResizeZone?>(null) }


    Box(
    Box(
        modifier =
        modifier =
@@ -190,53 +253,51 @@ private fun ResizableRectangle(
                .fillMaxSize()
                .fillMaxSize()
                // .onSizeChanged gives us the final size of this box, which is the screen size,
                // .onSizeChanged gives us the final size of this box, which is the screen size,
                // after it has been drawn.
                // after it has been drawn.
                .onSizeChanged { sizeInPixels ->
                .onSizeChanged { sizeInPixels: IntSize ->
                    screenWidth = sizeInPixels.width.toFloat()
                    state.screenWidth = sizeInPixels.width.toFloat()
                    screenHeight = sizeInPixels.height.toFloat()
                    state.screenHeight = sizeInPixels.height.toFloat()
                }
                }
    ) {
                .pointerInput(Unit) {
        Box(
            modifier =
                Modifier.graphicsLayer(translationX = rect.left, translationY = rect.top)
                    .size(
                        width = with(density) { rect.width.toDp() },
                        height = with(density) { rect.height.toDp() },
                    )
                    .border(borderStrokeWidth, MaterialTheme.colorScheme.onSurfaceVariant)
                    .pointerInput(screenWidth, screenHeight, onResizeDrag, onBoxDrag, onDragEnd) {
                    detectDragGestures(
                    detectDragGestures(
                            onDragStart = { startOffset ->
                        onDragStart = { startOffset: Offset -> state.startDrag(startOffset) },
                                draggedZone =
                        onDrag = { change: PointerInputChange, dragAmount: Offset ->
                                    getTouchedZone(
                            change.consume()
                                        boxWidth = size.width.toFloat(),
                            state.drag(change.position, dragAmount)
                                        boxHeight = size.height.toFloat(),
                                        startOffset = startOffset,
                                        touchAreaPx = touchAreaPx,
                                    )
                        },
                        },
                        onDragEnd = {
                        onDragEnd = {
                                draggedZone = null
                            state.dragEnd()
                                onDragEnd()
                            state.rect?.let { rect: Rect ->
                            },
                                // Store the rectangle to the ViewModel for taking a screenshot.
                            onDrag = { change, dragAmount ->
                                // The screenshot API requires a Rect class with int values.
                                change.consume()
                                onRegionSelected(

                                    IntRect(
                                // Create a stable and local copy of the draggedZone. This
                                        rect.left.roundToInt(),
                                // ensures that the value does not change in the onResizeDrag
                                        rect.top.roundToInt(),
                                // callback.
                                        rect.right.roundToInt(),
                                val currentZone = draggedZone
                                        rect.bottom.roundToInt(),

                                    )
                                if (currentZone != null) {
                                )
                                    // If currentZone has a value, it means we are dragging a zone
                                    // for resizing.
                                    onResizeDrag(dragAmount, currentZone, screenWidth, screenHeight)
                                } else {
                                    // If currentZone is null, it means we are dragging the box.
                                    onBoxDrag(dragAmount, screenWidth, screenHeight)
                            }
                            }
                        },
                        },
                        onDragCancel = { state.dragEnd() },
                    )
                    )
                    },
                }
    ) {
        // The width of the border stroke around the region box.
        val borderStrokeWidth = 4.dp

        state.rect?.let { currentRect ->
            Box(
                modifier =
                    Modifier.graphicsLayer(
                            translationX = currentRect.left,
                            translationY = currentRect.top,
                        )
                        .size(
                            width = with(density) { currentRect.width.toDp() },
                            height = with(density) { currentRect.height.toDp() },
                        )
                        .border(borderStrokeWidth, MaterialTheme.colorScheme.onSurfaceVariant),
                contentAlignment = Alignment.Center,
                contentAlignment = Alignment.Center,
            ) {
            ) {
                PrimaryButton(
                PrimaryButton(
@@ -254,3 +315,4 @@ private fun ResizableRectangle(
            }
            }
        }
        }
    }
    }
}