Loading packages/SystemUI/src/com/android/systemui/screencapture/ui/compose/RegionBox.kt +22 −108 Original line number Diff line number Diff line Loading @@ -43,32 +43,6 @@ import androidx.compose.ui.unit.dp import com.android.systemui.common.shared.model.Icon as IconModel import com.android.systemui.res.R /** * A common interface for all draggable zones of the resizable box. The ResizeZone is one of Corner * or Edge. */ sealed interface ResizeZone /** * 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) : ResizeZone { TopLeft(Alignment.TopStart), TopRight(Alignment.TopEnd), BottomLeft(Alignment.BottomStart), BottomRight(Alignment.BottomEnd), } /** An enum to identify each of the four edges of the rectangle. */ enum class Edge : ResizeZone { Top, Bottom, Left, Right, } /** * Determines which zone (corner or edge) of a box is being touched based on the press offset. * Loading @@ -78,35 +52,32 @@ enum class Edge : ResizeZone { * @param touchAreaPx The size of the touch area in pixels. * @return The ResizeZone that was pressed, or `null` if the press was not on a zone. */ private fun getDragZone( private fun getTouchedZone( boxWidth: Float, boxHeight: Float, startOffset: Offset, touchAreaPx: Float, ): ResizeZone? { val isTouchingTop = startOffset.y in -touchAreaPx..touchAreaPx val isTouchingBottom = startOffset.y in (boxHeight - touchAreaPx)..boxHeight val isTouchingBottom = startOffset.y in (boxHeight - touchAreaPx)..(boxHeight + touchAreaPx) val isTouchingLeft = startOffset.x in -touchAreaPx..touchAreaPx val isTouchingRight = startOffset.x in (boxWidth - touchAreaPx)..boxWidth val isTouchingRight = startOffset.x in (boxWidth - touchAreaPx)..(boxWidth + touchAreaPx) // Corners should be checked first because edges are also touched when the user touches a // corner. when { isTouchingTop && isTouchingLeft -> return Corner.TopLeft isTouchingTop && isTouchingRight -> return Corner.TopRight isTouchingBottom && isTouchingLeft -> return Corner.BottomLeft isTouchingBottom && isTouchingRight -> return Corner.BottomRight } return when { // Corners have priority over edges, as they occupy overlapping areas. isTouchingTop && isTouchingLeft -> ResizeZone.Corner.TopLeft isTouchingTop && isTouchingRight -> ResizeZone.Corner.TopRight isTouchingBottom && isTouchingLeft -> ResizeZone.Corner.BottomLeft isTouchingBottom && isTouchingRight -> ResizeZone.Corner.BottomRight // Edges are checked next. when { isTouchingTop -> return Edge.Top isTouchingBottom -> return Edge.Bottom isTouchingLeft -> return Edge.Left isTouchingRight -> return Edge.Right } // If not a corner, check for edges. isTouchingLeft -> ResizeZone.Edge.Left isTouchingTop -> ResizeZone.Edge.Top isTouchingRight -> ResizeZone.Edge.Right isTouchingBottom -> ResizeZone.Edge.Bottom return null else -> null } } /** Loading Loading @@ -144,65 +115,6 @@ fun RegionBox( ) } val onResizeDrag: (dragAmount: Offset, zone: ResizeZone, maxWidth: Float, maxHeight: Float) -> Unit = { dragAmount, zone, maxWidth, maxHeight -> 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. when (zone) { Corner.TopLeft, Corner.BottomLeft, Edge.Left -> { val potentialNewLeft = rect.left + dragX val rightLimitForMinWidth = rect.right - minSizePx newLeft = potentialNewLeft.coerceIn(0f, rightLimitForMinWidth) } Corner.TopRight, Corner.BottomRight, Edge.Right -> { val potentialNewRight = rect.right + dragX val leftLimitForMinWidth = rect.left + minSizePx newRight = potentialNewRight.coerceIn(leftLimitForMinWidth, maxWidth) } else -> { // No horizontal change for Top or Bottom edges. } } // Handle vertical drag for resizing. when (zone) { Corner.TopLeft, Corner.TopRight, Edge.Top -> { val potentialNewTop = rect.top + dragY val bottomLimitForMinHeight = rect.bottom - minSizePx newTop = potentialNewTop.coerceIn(0f, bottomLimitForMinHeight) } Corner.BottomLeft, Corner.BottomRight, Edge.Bottom -> { val potentialNewBottom = rect.bottom + dragY val topLimitForMinHeight = rect.top + minSizePx newBottom = potentialNewBottom.coerceIn(topLimitForMinHeight, maxHeight) } else -> { // No vertical change for Left or Right edges. } } rect = Rect(newLeft, newTop, newRight, newBottom) } val onBoxDrag: (dragAmount: Offset, maxWidth: Float, maxHeight: Float) -> Unit = { dragAmount, maxWidth, maxHeight -> val newOffset = rect.topLeft + dragAmount Loading @@ -220,7 +132,9 @@ fun RegionBox( ResizableRectangle( rect = rect, onResizeDrag = onResizeDrag, onResizeDrag = { dragAmount, zone, maxWidth, maxHeight -> rect = zone.processResizeDrag(rect, dragAmount, minSizePx, maxWidth, maxHeight) }, onBoxDrag = onBoxDrag, onDragEnd = { onDragEnd( Loading Loading @@ -294,7 +208,7 @@ private fun ResizableRectangle( detectDragGestures( onDragStart = { startOffset -> draggedZone = getDragZone( getTouchedZone( boxWidth = size.width.toFloat(), boxHeight = size.height.toFloat(), startOffset = startOffset, Loading packages/SystemUI/src/com/android/systemui/screencapture/ui/compose/ResizeZone.kt 0 → 100644 +191 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.screencapture.ui.compose import androidx.compose.ui.Alignment import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect // Helper functions to handle the calculation for each side of the rectangle. private fun newLeft(rect: Rect, dragAmount: Offset, minSizePx: Float): Float { return (rect.left + dragAmount.x).coerceIn(0f, rect.right - minSizePx) } private fun newTop(rect: Rect, dragAmount: Offset, minSizePx: Float): Float { return (rect.top + dragAmount.y).coerceIn(0f, rect.bottom - minSizePx) } private fun newRight(rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float): Float { return (rect.right + dragAmount.x).coerceIn(rect.left + minSizePx, maxWidth) } private fun newBottom(rect: Rect, dragAmount: Offset, minSizePx: Float, maxHeight: Float): Float { return (rect.bottom + dragAmount.y).coerceIn(rect.top + minSizePx, maxHeight) } /** * Defines the different zones of the box that can be dragged for resizing. A zone could be a Corner * or Edge. */ sealed interface ResizeZone { /** * Processes a drag gesture and calculates the new geometry of the rectangle. * * @param rect The current geometry of the rectangle. * @param dragAmount The amount of drag in pixels. * @param minSizePx The minimum size of the rectangle in pixels. * @param maxWidth The maximum width constraint for the rectangle. * @param maxHeight The maximum height constraint for the rectangle. * @return The updated Rect after processing the drag. */ fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect /** * A corner of the rectangle. * * @param alignment The alignment of the corner. */ sealed interface Corner : ResizeZone { val alignment: Alignment data object TopLeft : Corner { override val alignment = Alignment.TopStart override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newLeft = newLeft(rect, dragAmount, minSizePx) val newTop = newTop(rect, dragAmount, minSizePx) return Rect(newLeft, newTop, rect.right, rect.bottom) } } data object TopRight : Corner { override val alignment = Alignment.TopEnd override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newRight = newRight(rect, dragAmount, minSizePx, maxWidth) val newTop = newTop(rect, dragAmount, minSizePx) return Rect(rect.left, newTop, newRight, rect.bottom) } } data object BottomLeft : Corner { override val alignment = Alignment.BottomStart override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newLeft = newLeft(rect, dragAmount, minSizePx) val newBottom = newBottom(rect, dragAmount, minSizePx, maxHeight) return Rect(newLeft, rect.top, rect.right, newBottom) } } data object BottomRight : Corner { override val alignment = Alignment.BottomEnd override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newRight = newRight(rect, dragAmount, minSizePx, maxWidth) val newBottom = newBottom(rect, dragAmount, minSizePx, maxHeight) return Rect(rect.left, rect.top, newRight, newBottom) } } } /** An edge of the rectangle. */ sealed interface Edge : ResizeZone { data object Left : Edge { override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newLeft = newLeft(rect, dragAmount, minSizePx) return Rect(newLeft, rect.top, rect.right, rect.bottom) } } data object Top : Edge { override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newTop = newTop(rect, dragAmount, minSizePx) return Rect(rect.left, newTop, rect.right, rect.bottom) } } data object Right : Edge { override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newRight = newRight(rect, dragAmount, minSizePx, maxWidth) return Rect(rect.left, rect.top, newRight, rect.bottom) } } data object Bottom : Edge { override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newBottom = newBottom(rect, dragAmount, minSizePx, maxHeight) return Rect(rect.left, rect.top, rect.right, newBottom) } } } } Loading
packages/SystemUI/src/com/android/systemui/screencapture/ui/compose/RegionBox.kt +22 −108 Original line number Diff line number Diff line Loading @@ -43,32 +43,6 @@ import androidx.compose.ui.unit.dp import com.android.systemui.common.shared.model.Icon as IconModel import com.android.systemui.res.R /** * A common interface for all draggable zones of the resizable box. The ResizeZone is one of Corner * or Edge. */ sealed interface ResizeZone /** * 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) : ResizeZone { TopLeft(Alignment.TopStart), TopRight(Alignment.TopEnd), BottomLeft(Alignment.BottomStart), BottomRight(Alignment.BottomEnd), } /** An enum to identify each of the four edges of the rectangle. */ enum class Edge : ResizeZone { Top, Bottom, Left, Right, } /** * Determines which zone (corner or edge) of a box is being touched based on the press offset. * Loading @@ -78,35 +52,32 @@ enum class Edge : ResizeZone { * @param touchAreaPx The size of the touch area in pixels. * @return The ResizeZone that was pressed, or `null` if the press was not on a zone. */ private fun getDragZone( private fun getTouchedZone( boxWidth: Float, boxHeight: Float, startOffset: Offset, touchAreaPx: Float, ): ResizeZone? { val isTouchingTop = startOffset.y in -touchAreaPx..touchAreaPx val isTouchingBottom = startOffset.y in (boxHeight - touchAreaPx)..boxHeight val isTouchingBottom = startOffset.y in (boxHeight - touchAreaPx)..(boxHeight + touchAreaPx) val isTouchingLeft = startOffset.x in -touchAreaPx..touchAreaPx val isTouchingRight = startOffset.x in (boxWidth - touchAreaPx)..boxWidth val isTouchingRight = startOffset.x in (boxWidth - touchAreaPx)..(boxWidth + touchAreaPx) // Corners should be checked first because edges are also touched when the user touches a // corner. when { isTouchingTop && isTouchingLeft -> return Corner.TopLeft isTouchingTop && isTouchingRight -> return Corner.TopRight isTouchingBottom && isTouchingLeft -> return Corner.BottomLeft isTouchingBottom && isTouchingRight -> return Corner.BottomRight } return when { // Corners have priority over edges, as they occupy overlapping areas. isTouchingTop && isTouchingLeft -> ResizeZone.Corner.TopLeft isTouchingTop && isTouchingRight -> ResizeZone.Corner.TopRight isTouchingBottom && isTouchingLeft -> ResizeZone.Corner.BottomLeft isTouchingBottom && isTouchingRight -> ResizeZone.Corner.BottomRight // Edges are checked next. when { isTouchingTop -> return Edge.Top isTouchingBottom -> return Edge.Bottom isTouchingLeft -> return Edge.Left isTouchingRight -> return Edge.Right } // If not a corner, check for edges. isTouchingLeft -> ResizeZone.Edge.Left isTouchingTop -> ResizeZone.Edge.Top isTouchingRight -> ResizeZone.Edge.Right isTouchingBottom -> ResizeZone.Edge.Bottom return null else -> null } } /** Loading Loading @@ -144,65 +115,6 @@ fun RegionBox( ) } val onResizeDrag: (dragAmount: Offset, zone: ResizeZone, maxWidth: Float, maxHeight: Float) -> Unit = { dragAmount, zone, maxWidth, maxHeight -> 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. when (zone) { Corner.TopLeft, Corner.BottomLeft, Edge.Left -> { val potentialNewLeft = rect.left + dragX val rightLimitForMinWidth = rect.right - minSizePx newLeft = potentialNewLeft.coerceIn(0f, rightLimitForMinWidth) } Corner.TopRight, Corner.BottomRight, Edge.Right -> { val potentialNewRight = rect.right + dragX val leftLimitForMinWidth = rect.left + minSizePx newRight = potentialNewRight.coerceIn(leftLimitForMinWidth, maxWidth) } else -> { // No horizontal change for Top or Bottom edges. } } // Handle vertical drag for resizing. when (zone) { Corner.TopLeft, Corner.TopRight, Edge.Top -> { val potentialNewTop = rect.top + dragY val bottomLimitForMinHeight = rect.bottom - minSizePx newTop = potentialNewTop.coerceIn(0f, bottomLimitForMinHeight) } Corner.BottomLeft, Corner.BottomRight, Edge.Bottom -> { val potentialNewBottom = rect.bottom + dragY val topLimitForMinHeight = rect.top + minSizePx newBottom = potentialNewBottom.coerceIn(topLimitForMinHeight, maxHeight) } else -> { // No vertical change for Left or Right edges. } } rect = Rect(newLeft, newTop, newRight, newBottom) } val onBoxDrag: (dragAmount: Offset, maxWidth: Float, maxHeight: Float) -> Unit = { dragAmount, maxWidth, maxHeight -> val newOffset = rect.topLeft + dragAmount Loading @@ -220,7 +132,9 @@ fun RegionBox( ResizableRectangle( rect = rect, onResizeDrag = onResizeDrag, onResizeDrag = { dragAmount, zone, maxWidth, maxHeight -> rect = zone.processResizeDrag(rect, dragAmount, minSizePx, maxWidth, maxHeight) }, onBoxDrag = onBoxDrag, onDragEnd = { onDragEnd( Loading Loading @@ -294,7 +208,7 @@ private fun ResizableRectangle( detectDragGestures( onDragStart = { startOffset -> draggedZone = getDragZone( getTouchedZone( boxWidth = size.width.toFloat(), boxHeight = size.height.toFloat(), startOffset = startOffset, Loading
packages/SystemUI/src/com/android/systemui/screencapture/ui/compose/ResizeZone.kt 0 → 100644 +191 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.screencapture.ui.compose import androidx.compose.ui.Alignment import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect // Helper functions to handle the calculation for each side of the rectangle. private fun newLeft(rect: Rect, dragAmount: Offset, minSizePx: Float): Float { return (rect.left + dragAmount.x).coerceIn(0f, rect.right - minSizePx) } private fun newTop(rect: Rect, dragAmount: Offset, minSizePx: Float): Float { return (rect.top + dragAmount.y).coerceIn(0f, rect.bottom - minSizePx) } private fun newRight(rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float): Float { return (rect.right + dragAmount.x).coerceIn(rect.left + minSizePx, maxWidth) } private fun newBottom(rect: Rect, dragAmount: Offset, minSizePx: Float, maxHeight: Float): Float { return (rect.bottom + dragAmount.y).coerceIn(rect.top + minSizePx, maxHeight) } /** * Defines the different zones of the box that can be dragged for resizing. A zone could be a Corner * or Edge. */ sealed interface ResizeZone { /** * Processes a drag gesture and calculates the new geometry of the rectangle. * * @param rect The current geometry of the rectangle. * @param dragAmount The amount of drag in pixels. * @param minSizePx The minimum size of the rectangle in pixels. * @param maxWidth The maximum width constraint for the rectangle. * @param maxHeight The maximum height constraint for the rectangle. * @return The updated Rect after processing the drag. */ fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect /** * A corner of the rectangle. * * @param alignment The alignment of the corner. */ sealed interface Corner : ResizeZone { val alignment: Alignment data object TopLeft : Corner { override val alignment = Alignment.TopStart override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newLeft = newLeft(rect, dragAmount, minSizePx) val newTop = newTop(rect, dragAmount, minSizePx) return Rect(newLeft, newTop, rect.right, rect.bottom) } } data object TopRight : Corner { override val alignment = Alignment.TopEnd override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newRight = newRight(rect, dragAmount, minSizePx, maxWidth) val newTop = newTop(rect, dragAmount, minSizePx) return Rect(rect.left, newTop, newRight, rect.bottom) } } data object BottomLeft : Corner { override val alignment = Alignment.BottomStart override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newLeft = newLeft(rect, dragAmount, minSizePx) val newBottom = newBottom(rect, dragAmount, minSizePx, maxHeight) return Rect(newLeft, rect.top, rect.right, newBottom) } } data object BottomRight : Corner { override val alignment = Alignment.BottomEnd override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newRight = newRight(rect, dragAmount, minSizePx, maxWidth) val newBottom = newBottom(rect, dragAmount, minSizePx, maxHeight) return Rect(rect.left, rect.top, newRight, newBottom) } } } /** An edge of the rectangle. */ sealed interface Edge : ResizeZone { data object Left : Edge { override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newLeft = newLeft(rect, dragAmount, minSizePx) return Rect(newLeft, rect.top, rect.right, rect.bottom) } } data object Top : Edge { override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newTop = newTop(rect, dragAmount, minSizePx) return Rect(rect.left, newTop, rect.right, rect.bottom) } } data object Right : Edge { override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newRight = newRight(rect, dragAmount, minSizePx, maxWidth) return Rect(rect.left, rect.top, newRight, rect.bottom) } } data object Bottom : Edge { override fun processResizeDrag( rect: Rect, dragAmount: Offset, minSizePx: Float, maxWidth: Float, maxHeight: Float, ): Rect { val newBottom = newBottom(rect, dragAmount, minSizePx, maxHeight) return Rect(rect.left, rect.top, rect.right, newBottom) } } } }