/* * 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.eyedropper.ui.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.integerResource import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import com.android.eyedropper.R import com.android.eyedropper.ui.utils.ComposableDrawableLoader.drawableToBitmap const val ARROW_PADDING_PX = 20f // Distance from inner circle edge to arrow center const val ARROW_TOUCH_SLOP_PX = 12f // Expand touch target for easier tapping const val LONG_PRESS_MOVE_PX = 10 enum class ReticleDirectionButton { UP, DOWN, LEFT, RIGHT } // Data class to hold pre-loaded resources and pixel dimensions data class ReticleProps( val reticleBackgroundBitmap: ImageBitmap, val reticleOutlineBitmap: ImageBitmap, val handleBitmap: ImageBitmap, val arrowUpBitmap: ImageBitmap, val arrowDownBitmap: ImageBitmap, val arrowLeftBitmap: ImageBitmap, val arrowRightBitmap: ImageBitmap, val reticleBackgroundWidthPx: Float, val reticleBackgroundHeightPx: Float, val reticleInnerCircleSizePx: Float, val reticleRingWidthPx: Float, val handleMarginTopPx: Float, val handleMarginBottomPx: Float, val handleWidthPx: Float, val handleHeightPx: Float, val googleGrey400: Color, val containerColor: Color, val reticleInnerOutlineStrokeWidthPx: Float, val reticleToScreenBorderMinPadding: Float, val magnifiedPatchNumPx: Int, val keyMoveAmountPx: Int, ) // Composable to load and remember ReticleDrawData @Composable fun getReticleDrawProps(density: Density): ReticleProps { // Load bitmaps val reticleBackgroundBitmap = drawableToBitmap(R.drawable.magnify_background) val reticleOutlineBitmap = drawableToBitmap(R.drawable.magnify_background_outline) val handleBitmap = drawableToBitmap(R.drawable.reticle_handle) val arrowUpBitmap = drawableToBitmap(R.drawable.reticle_up_arrow) val arrowDownBitmap = drawableToBitmap(R.drawable.reticle_down_arrow) val arrowLeftBitmap = drawableToBitmap(R.drawable.reticle_left_arrow) val arrowRightBitmap = drawableToBitmap(R.drawable.reticle_right_arrow) val reticleBackgroundWidthPx = dimensionResource( R.dimen.reticle_magnify_view_background_width ).toPx(density) val reticleBackgroundHeightPx = dimensionResource( R.dimen.reticle_magnify_view_background_height ).toPx(density) val reticleInnerCircleSizePx = dimensionResource( R.dimen.reticle_magnify_view_inner_circle_size ).toPx(density) val reticleRingWidthPx = dimensionResource( R.dimen.reticle_magnify_view_outer_ring_width ).toPx(density) val handleMarginTopPx = dimensionResource(R.dimen.reticle_handle_view_margin_top).toPx(density) val handleMarginBottomPx = dimensionResource( R.dimen.reticle_handle_view_margin_bottom ).toPx(density) val handleWidthPx = handleBitmap.width.toFloat() val handleHeightPx = handleBitmap.height.toFloat() val googleGrey400 = colorResource(R.color.google_grey400) val containerColor = colorResource(R.color.screenshot_image_view_container_bg_color) val reticleInnerOutlineStrokeWidthPx = dimensionResource( R.dimen.reticle_handle_view_inner_outline_stroke_width ).toPx(density) val magnifiedPatchNumPx = integerResource(R.integer.touchscreen_magnified_patch_num_px) val reticleToScreenBorderMinPadding = dimensionResource( R.dimen.reticle_to_screen_border_min_padding ).toPx(density) val keyMoveAmountPx = integerResource(R.integer.key_move_amount_px) return remember(density) { ReticleProps( reticleBackgroundBitmap = reticleBackgroundBitmap, reticleOutlineBitmap = reticleOutlineBitmap, handleBitmap = handleBitmap, arrowUpBitmap = arrowUpBitmap, arrowDownBitmap = arrowDownBitmap, arrowLeftBitmap = arrowLeftBitmap, arrowRightBitmap = arrowRightBitmap, reticleBackgroundWidthPx = reticleBackgroundWidthPx, reticleBackgroundHeightPx = reticleBackgroundHeightPx, reticleInnerCircleSizePx = reticleInnerCircleSizePx, reticleInnerOutlineStrokeWidthPx = reticleInnerOutlineStrokeWidthPx, reticleRingWidthPx = reticleRingWidthPx, magnifiedPatchNumPx = magnifiedPatchNumPx, handleMarginTopPx = handleMarginTopPx, handleMarginBottomPx = handleMarginBottomPx, handleWidthPx = handleWidthPx, handleHeightPx = handleHeightPx, googleGrey400 = googleGrey400, containerColor = containerColor, reticleToScreenBorderMinPadding = reticleToScreenBorderMinPadding, keyMoveAmountPx = keyMoveAmountPx, ) } } /** * Get coordinates for the rectangular bounding box around the reticle at [displayedPointerPosition] */ fun getReticleTotalBounds(displayedPointerPosition: Offset, drawProps: ReticleProps): Rect { val handleTopY = displayedPointerPosition.y - drawProps.handleHeightPx / 2f val reticleBackgroundTopY = handleTopY - drawProps.handleMarginBottomPx - drawProps.handleMarginTopPx - drawProps.reticleBackgroundHeightPx val left = displayedPointerPosition.x - drawProps.reticleBackgroundWidthPx / 2f val right = displayedPointerPosition.x + drawProps.reticleBackgroundWidthPx / 2f val top = reticleBackgroundTopY val bottom = displayedPointerPosition.y + drawProps.handleHeightPx / 2f return Rect(left, top, right, bottom) } /** * Check if the tap at [this] is within the touch target region for the [direction] arrow. */ fun Offset.isWithinArrow( direction: ReticleDirectionButton, bitmap: ImageBitmap, innerCircleCenter: Offset, placementRadius: Int, ): Boolean { val width = bitmap.width val height = bitmap.height val topLeft = innerCircleCenter.getArrowPositionAroundCenter( direction, width / 2, placementRadius ) val touchArea = Rect( topLeft.x - ARROW_TOUCH_SLOP_PX, topLeft.y - ARROW_TOUCH_SLOP_PX, topLeft.x + width + ARROW_TOUCH_SLOP_PX, topLeft.y + height + ARROW_TOUCH_SLOP_PX ) return touchArea.contains(this) } /** * Get the top left position the [direction] arrow should be placed */ fun Offset.getArrowPositionAroundCenter( direction: ReticleDirectionButton, itemRadius: Int, arcRadius: Int ): Offset { val topLeftPos = when (direction) { ReticleDirectionButton.UP -> this.copy(y = this.y - arcRadius) ReticleDirectionButton.DOWN -> this.copy(y = this.y + arcRadius) ReticleDirectionButton.LEFT -> this.copy(x = this.x - arcRadius) ReticleDirectionButton.RIGHT -> this.copy(x = this.x + arcRadius) } return Offset(topLeftPos.x - itemRadius, topLeftPos.y - itemRadius) } fun Dp.toPx(density: Density): Float = with(density) { this@toPx.toPx() }