Loading packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/compose/PreCaptureUI.kt +2 −7 Original line number Original line Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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, ) ) } } Loading packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/compose/RegionBox.kt +197 −135 Original line number Original line Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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 Loading @@ -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 = Loading @@ -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( Loading @@ -254,3 +315,4 @@ private fun ResizableRectangle( } } } } } } } Loading
packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/compose/PreCaptureUI.kt +2 −7 Original line number Original line Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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, ) ) } } Loading
packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/compose/RegionBox.kt +197 −135 Original line number Original line Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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 Loading @@ -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 = Loading @@ -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( Loading @@ -254,3 +315,4 @@ private fun ResizableRectangle( } } } } } } }