Loading packages/SystemUI/src/com/android/systemui/screencapture/ui/compose/RegionBox.kt +128 −132 Original line number Diff line number Diff line Loading @@ -33,82 +33,112 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.min /** An enum to identify each of the four corners of the rectangle. */ enum class Corner { TopLeft, TopRight, BottomLeft, BottomRight, /** * An enum to identify each of the four corners of the rectangle. * * @param alignment The alignment of the corner within the box. */ enum class Corner(val alignment: Alignment) { TopLeft(Alignment.TopStart), TopRight(Alignment.TopEnd), BottomLeft(Alignment.BottomStart), BottomRight(Alignment.BottomEnd), } /** * A stateful composable that manages the size of a resizable RegionBox. * A stateful composable that manages the size and position of a resizable RegionBox. * * @param initialWidth The initial width of the box. * @param initialHeight The initial height of the box. * @param onDragEnd A callback function that is invoked with the final width and height when the * user finishes a drag gesture. * @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 fun RegionBox( initialWidth: Dp, initialHeight: Dp, onDragEnd: (width: Dp, height: Dp) -> Unit, onDragEnd: (offset: Offset, width: Dp, height: Dp) -> Unit, initialOffset: Offset = Offset.Zero, modifier: Modifier = Modifier, ) { // The minimum size allowed for the rectangle. // TODO(b/422565042): change when its value is finalized. val MIN_SIZE = 48.dp val minSize = 48.dp // The current state of the box. var width by remember { mutableStateOf(initialWidth) } var height by remember { mutableStateOf(initialHeight) } val density = LocalDensity.current val minSizePx = remember(density) { with(density) { minSize.toPx() } } val onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Dp, maxHeight: Dp) -> Unit = { dragAmount, corner, maxWidth, maxHeight -> val (dragX, dragY) = dragAmount // State for the region box's geometry. var rect by remember { mutableStateOf( with(density) { // Calculate the potential new width and height based on the drag amount. // The width and height are increased or decreased by twice the drag amount as the // drag is calculated from the center of the box, so we need to add twice the drag // amount to account for both sides of the box. val newWidth = when (corner) { Corner.TopLeft, Corner.BottomLeft -> width - (dragX.toDp() * 2) Corner.TopRight, Corner.BottomRight -> width + (dragX.toDp() * 2) // offset is how far from the parent's top-left corner the box should be placed. Rect(offset = initialOffset, size = Size(initialWidth.toPx(), initialHeight.toPx())) } val newHeight = when (corner) { Corner.TopLeft, Corner.TopRight -> height - (dragY.toDp() * 2) Corner.BottomLeft, Corner.BottomRight -> height + (dragY.toDp() * 2) ) } // The new width and height cannot be smaller than the minimum allowed, or bigger // than the screen size. width = max(min(newWidth, maxWidth), MIN_SIZE) height = max(min(newHeight, maxHeight), MIN_SIZE) val onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Float, maxHeight: Float) -> Unit = { dragAmount, corner, maxWidth, maxHeight -> // Used for calculating the new dimensions based on which corner is dragged. var newLeft = rect.left var newTop = rect.top var newRight = rect.right var newBottom = rect.bottom val (dragX, dragY) = dragAmount // Handle horizontal drag for resizing. if (corner == Corner.TopLeft || corner == Corner.BottomLeft) { val potentialNewLeft = rect.left + dragX val rightLimitForMinWidth = rect.right - minSizePx newLeft = potentialNewLeft.coerceIn(0f, rightLimitForMinWidth) } else { val potentialNewRight = rect.right + dragX val leftLimitForMinWidth = rect.left + minSizePx newRight = potentialNewRight.coerceIn(leftLimitForMinWidth, maxWidth) } // Handle vertical drag for resizing. if (corner == Corner.TopLeft || corner == Corner.TopRight) { val potentialNewTop = rect.top + dragY val bottomLimitForMinHeight = rect.bottom - minSizePx newTop = potentialNewTop.coerceIn(0f, bottomLimitForMinHeight) } else { val potentialNewBottom = rect.bottom + dragY val topLimitForMinHeight = rect.top + minSizePx newBottom = potentialNewBottom.coerceIn(topLimitForMinHeight, maxHeight) } rect = Rect(newLeft, newTop, newRight, newBottom) } ResizableRectangle( width = width, height = height, rect = rect, onDrag = onDrag, onDragEnd = { onDragEnd(width, height) }, onDragEnd = { onDragEnd( Offset(rect.left, rect.top), with(density) { rect.width.toDp() }, with(density) { rect.height.toDp() }, ) }, modifier = modifier, ) } Loading @@ -116,28 +146,27 @@ fun RegionBox( /** * A box with border lines and centered corner knobs that can be resized. * * @param width The current width of the box. * @param height The current height of the box. * @param rect The current geometry of the region box. * @param onDrag Callback invoked when a knob is dragged. * @param onDragEnd Callback invoked when a drag gesture finishes. * @param modifier The modifier to be applied to the composable. */ @Composable private fun ResizableRectangle( width: Dp, height: Dp, onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Dp, maxHeight: Dp) -> Unit, rect: Rect, onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Float, maxHeight: Float) -> Unit, onDragEnd: () -> Unit, modifier: Modifier = Modifier, ) { // The diameter of the resizable knob on each corner of the region box. val KNOB_DIAMETER = 8.dp val knobDiameter = 8.dp // The width of the border stroke around the region box. val BORDER_STROKE_WIDTH = 4.dp val borderStrokeWidth = 4.dp // Must remember the screen size for the drag logic. Initial values are set to 0. var screenWidth by remember { mutableStateOf(0.dp) } var screenHeight by remember { mutableStateOf(0.dp) } var screenWidth by remember { mutableStateOf(0f) } var screenHeight by remember { mutableStateOf(0f) } val density = LocalDensity.current // The box that contains the whole screen. Loading @@ -148,91 +177,57 @@ private fun ResizableRectangle( // .onSizeChanged gives us the final size of this box, which is the screen size, // after it has been drawn. .onSizeChanged { sizeInPixels -> screenWidth = with(density) { sizeInPixels.width.toDp() } screenHeight = with(density) { sizeInPixels.height.toDp() } }, contentAlignment = Alignment.Center, screenWidth = sizeInPixels.width.toFloat() screenHeight = sizeInPixels.height.toFloat() } ) { // The box container for the region box and its knobs. Box(modifier = Modifier.size(width, height)) { Box( modifier = Modifier.graphicsLayer(translationX = rect.left, translationY = rect.top) .size( width = with(density) { rect.width.toDp() }, height = with(density) { rect.height.toDp() }, ) ) { // The main box for the region selection. Box( modifier = Modifier.fillMaxSize() .border(BORDER_STROKE_WIDTH, MaterialTheme.colorScheme.onSurfaceVariant) .border(borderStrokeWidth, MaterialTheme.colorScheme.onSurfaceVariant) ) // The offset is half of the knob diameter so that it is centered. val knobOffset = KNOB_DIAMETER / 2 val knobOffset = knobDiameter / 2 // Top left knob Knob( diameter = KNOB_DIAMETER, modifier = Modifier.align(Alignment.TopStart) .offset(x = -knobOffset, y = -knobOffset) .pointerInput(Unit) { detectDragGestures( onDragEnd = onDragEnd, onDrag = { change, dragAmount -> change.consume() onDrag(dragAmount, Corner.TopLeft, screenWidth, screenHeight) }, ) }, ) // Create knobs by looping through the Corner enum values Corner.entries.forEach { corner -> val xOffset: Dp val yOffset: Dp // Top right knob Knob( diameter = KNOB_DIAMETER, modifier = Modifier.align(Alignment.TopEnd) .offset(x = knobOffset, y = -knobOffset) .pointerInput(Unit) { detectDragGestures( onDragEnd = onDragEnd, onDrag = { change, dragAmount -> change.consume() onDrag(dragAmount, Corner.TopRight, screenWidth, screenHeight) }, ) }, ) if (corner == Corner.TopLeft || corner == Corner.BottomLeft) { xOffset = -knobOffset } else { xOffset = knobOffset } // Bottom left knob Knob( diameter = KNOB_DIAMETER, modifier = Modifier.align(Alignment.BottomStart) .offset(x = -knobOffset, y = knobOffset) .pointerInput(Unit) { detectDragGestures( onDragEnd = onDragEnd, onDrag = { change, dragAmount -> change.consume() onDrag(dragAmount, Corner.BottomLeft, screenWidth, screenHeight) }, ) }, ) if (corner == Corner.TopLeft || corner == Corner.TopRight) { yOffset = -knobOffset } else { yOffset = knobOffset } // Bottom right knob Knob( diameter = KNOB_DIAMETER, diameter = knobDiameter, modifier = Modifier.align(Alignment.BottomEnd) .offset(x = knobOffset, y = knobOffset) .pointerInput(Unit) { Modifier.align(corner.alignment) .offset(x = xOffset, y = yOffset) .pointerInput(corner, screenWidth, screenHeight) { detectDragGestures( onDragEnd = onDragEnd, onDrag = { change, dragAmount -> change.consume() onDrag( dragAmount, Corner.BottomRight, screenWidth, screenHeight, ) onDrag(dragAmount, corner, screenWidth, screenHeight) }, ) }, Loading @@ -240,6 +235,7 @@ private fun ResizableRectangle( } } } } /** * The circular knob on each corner of the box used for dragging each corner. Loading Loading
packages/SystemUI/src/com/android/systemui/screencapture/ui/compose/RegionBox.kt +128 −132 Original line number Diff line number Diff line Loading @@ -33,82 +33,112 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.min /** An enum to identify each of the four corners of the rectangle. */ enum class Corner { TopLeft, TopRight, BottomLeft, BottomRight, /** * An enum to identify each of the four corners of the rectangle. * * @param alignment The alignment of the corner within the box. */ enum class Corner(val alignment: Alignment) { TopLeft(Alignment.TopStart), TopRight(Alignment.TopEnd), BottomLeft(Alignment.BottomStart), BottomRight(Alignment.BottomEnd), } /** * A stateful composable that manages the size of a resizable RegionBox. * A stateful composable that manages the size and position of a resizable RegionBox. * * @param initialWidth The initial width of the box. * @param initialHeight The initial height of the box. * @param onDragEnd A callback function that is invoked with the final width and height when the * user finishes a drag gesture. * @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 fun RegionBox( initialWidth: Dp, initialHeight: Dp, onDragEnd: (width: Dp, height: Dp) -> Unit, onDragEnd: (offset: Offset, width: Dp, height: Dp) -> Unit, initialOffset: Offset = Offset.Zero, modifier: Modifier = Modifier, ) { // The minimum size allowed for the rectangle. // TODO(b/422565042): change when its value is finalized. val MIN_SIZE = 48.dp val minSize = 48.dp // The current state of the box. var width by remember { mutableStateOf(initialWidth) } var height by remember { mutableStateOf(initialHeight) } val density = LocalDensity.current val minSizePx = remember(density) { with(density) { minSize.toPx() } } val onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Dp, maxHeight: Dp) -> Unit = { dragAmount, corner, maxWidth, maxHeight -> val (dragX, dragY) = dragAmount // State for the region box's geometry. var rect by remember { mutableStateOf( with(density) { // Calculate the potential new width and height based on the drag amount. // The width and height are increased or decreased by twice the drag amount as the // drag is calculated from the center of the box, so we need to add twice the drag // amount to account for both sides of the box. val newWidth = when (corner) { Corner.TopLeft, Corner.BottomLeft -> width - (dragX.toDp() * 2) Corner.TopRight, Corner.BottomRight -> width + (dragX.toDp() * 2) // offset is how far from the parent's top-left corner the box should be placed. Rect(offset = initialOffset, size = Size(initialWidth.toPx(), initialHeight.toPx())) } val newHeight = when (corner) { Corner.TopLeft, Corner.TopRight -> height - (dragY.toDp() * 2) Corner.BottomLeft, Corner.BottomRight -> height + (dragY.toDp() * 2) ) } // The new width and height cannot be smaller than the minimum allowed, or bigger // than the screen size. width = max(min(newWidth, maxWidth), MIN_SIZE) height = max(min(newHeight, maxHeight), MIN_SIZE) val onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Float, maxHeight: Float) -> Unit = { dragAmount, corner, maxWidth, maxHeight -> // Used for calculating the new dimensions based on which corner is dragged. var newLeft = rect.left var newTop = rect.top var newRight = rect.right var newBottom = rect.bottom val (dragX, dragY) = dragAmount // Handle horizontal drag for resizing. if (corner == Corner.TopLeft || corner == Corner.BottomLeft) { val potentialNewLeft = rect.left + dragX val rightLimitForMinWidth = rect.right - minSizePx newLeft = potentialNewLeft.coerceIn(0f, rightLimitForMinWidth) } else { val potentialNewRight = rect.right + dragX val leftLimitForMinWidth = rect.left + minSizePx newRight = potentialNewRight.coerceIn(leftLimitForMinWidth, maxWidth) } // Handle vertical drag for resizing. if (corner == Corner.TopLeft || corner == Corner.TopRight) { val potentialNewTop = rect.top + dragY val bottomLimitForMinHeight = rect.bottom - minSizePx newTop = potentialNewTop.coerceIn(0f, bottomLimitForMinHeight) } else { val potentialNewBottom = rect.bottom + dragY val topLimitForMinHeight = rect.top + minSizePx newBottom = potentialNewBottom.coerceIn(topLimitForMinHeight, maxHeight) } rect = Rect(newLeft, newTop, newRight, newBottom) } ResizableRectangle( width = width, height = height, rect = rect, onDrag = onDrag, onDragEnd = { onDragEnd(width, height) }, onDragEnd = { onDragEnd( Offset(rect.left, rect.top), with(density) { rect.width.toDp() }, with(density) { rect.height.toDp() }, ) }, modifier = modifier, ) } Loading @@ -116,28 +146,27 @@ fun RegionBox( /** * A box with border lines and centered corner knobs that can be resized. * * @param width The current width of the box. * @param height The current height of the box. * @param rect The current geometry of the region box. * @param onDrag Callback invoked when a knob is dragged. * @param onDragEnd Callback invoked when a drag gesture finishes. * @param modifier The modifier to be applied to the composable. */ @Composable private fun ResizableRectangle( width: Dp, height: Dp, onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Dp, maxHeight: Dp) -> Unit, rect: Rect, onDrag: (dragAmount: Offset, corner: Corner, maxWidth: Float, maxHeight: Float) -> Unit, onDragEnd: () -> Unit, modifier: Modifier = Modifier, ) { // The diameter of the resizable knob on each corner of the region box. val KNOB_DIAMETER = 8.dp val knobDiameter = 8.dp // The width of the border stroke around the region box. val BORDER_STROKE_WIDTH = 4.dp val borderStrokeWidth = 4.dp // Must remember the screen size for the drag logic. Initial values are set to 0. var screenWidth by remember { mutableStateOf(0.dp) } var screenHeight by remember { mutableStateOf(0.dp) } var screenWidth by remember { mutableStateOf(0f) } var screenHeight by remember { mutableStateOf(0f) } val density = LocalDensity.current // The box that contains the whole screen. Loading @@ -148,91 +177,57 @@ private fun ResizableRectangle( // .onSizeChanged gives us the final size of this box, which is the screen size, // after it has been drawn. .onSizeChanged { sizeInPixels -> screenWidth = with(density) { sizeInPixels.width.toDp() } screenHeight = with(density) { sizeInPixels.height.toDp() } }, contentAlignment = Alignment.Center, screenWidth = sizeInPixels.width.toFloat() screenHeight = sizeInPixels.height.toFloat() } ) { // The box container for the region box and its knobs. Box(modifier = Modifier.size(width, height)) { Box( modifier = Modifier.graphicsLayer(translationX = rect.left, translationY = rect.top) .size( width = with(density) { rect.width.toDp() }, height = with(density) { rect.height.toDp() }, ) ) { // The main box for the region selection. Box( modifier = Modifier.fillMaxSize() .border(BORDER_STROKE_WIDTH, MaterialTheme.colorScheme.onSurfaceVariant) .border(borderStrokeWidth, MaterialTheme.colorScheme.onSurfaceVariant) ) // The offset is half of the knob diameter so that it is centered. val knobOffset = KNOB_DIAMETER / 2 val knobOffset = knobDiameter / 2 // Top left knob Knob( diameter = KNOB_DIAMETER, modifier = Modifier.align(Alignment.TopStart) .offset(x = -knobOffset, y = -knobOffset) .pointerInput(Unit) { detectDragGestures( onDragEnd = onDragEnd, onDrag = { change, dragAmount -> change.consume() onDrag(dragAmount, Corner.TopLeft, screenWidth, screenHeight) }, ) }, ) // Create knobs by looping through the Corner enum values Corner.entries.forEach { corner -> val xOffset: Dp val yOffset: Dp // Top right knob Knob( diameter = KNOB_DIAMETER, modifier = Modifier.align(Alignment.TopEnd) .offset(x = knobOffset, y = -knobOffset) .pointerInput(Unit) { detectDragGestures( onDragEnd = onDragEnd, onDrag = { change, dragAmount -> change.consume() onDrag(dragAmount, Corner.TopRight, screenWidth, screenHeight) }, ) }, ) if (corner == Corner.TopLeft || corner == Corner.BottomLeft) { xOffset = -knobOffset } else { xOffset = knobOffset } // Bottom left knob Knob( diameter = KNOB_DIAMETER, modifier = Modifier.align(Alignment.BottomStart) .offset(x = -knobOffset, y = knobOffset) .pointerInput(Unit) { detectDragGestures( onDragEnd = onDragEnd, onDrag = { change, dragAmount -> change.consume() onDrag(dragAmount, Corner.BottomLeft, screenWidth, screenHeight) }, ) }, ) if (corner == Corner.TopLeft || corner == Corner.TopRight) { yOffset = -knobOffset } else { yOffset = knobOffset } // Bottom right knob Knob( diameter = KNOB_DIAMETER, diameter = knobDiameter, modifier = Modifier.align(Alignment.BottomEnd) .offset(x = knobOffset, y = knobOffset) .pointerInput(Unit) { Modifier.align(corner.alignment) .offset(x = xOffset, y = yOffset) .pointerInput(corner, screenWidth, screenHeight) { detectDragGestures( onDragEnd = onDragEnd, onDrag = { change, dragAmount -> change.consume() onDrag( dragAmount, Corner.BottomRight, screenWidth, screenHeight, ) onDrag(dragAmount, corner, screenWidth, screenHeight) }, ) }, Loading @@ -240,6 +235,7 @@ private fun ResizableRectangle( } } } } /** * The circular knob on each corner of the box used for dragging each corner. Loading