109 lines
4.6 KiB
Kotlin
109 lines
4.6 KiB
Kotlin
package uts.sdk.modules.masCodeScan
|
|
|
|
import android.graphics.Rect
|
|
import android.graphics.RectF
|
|
import kotlin.math.max
|
|
import kotlin.math.min
|
|
|
|
/**
|
|
* 相对于实时预览层的有效扫码区域配置。
|
|
* 由 Activity 统一校验后同时用于扫描框绘制和条码结果过滤。
|
|
*/
|
|
data class ScanRegionConfig(
|
|
val enabled: Boolean,
|
|
val widthRatio: Float = DEFAULT_SIZE_RATIO,
|
|
val heightRatio: Float = DEFAULT_SIZE_RATIO,
|
|
val centerXRatio: Float = DEFAULT_CENTER_RATIO,
|
|
val centerYRatio: Float = DEFAULT_CENTER_RATIO
|
|
) {
|
|
companion object {
|
|
const val DEFAULT_SIZE_RATIO = 0.74f
|
|
const val DEFAULT_CENTER_RATIO = 0.5f
|
|
private const val MIN_SIZE_RATIO = 0.1f
|
|
private const val MAX_SIZE_RATIO = 1f
|
|
|
|
fun safe(
|
|
enabled: Boolean,
|
|
widthRatio: Float,
|
|
heightRatio: Float,
|
|
centerXRatio: Float,
|
|
centerYRatio: Float
|
|
): ScanRegionConfig {
|
|
val safeWidth = widthRatio.sanitize(DEFAULT_SIZE_RATIO).coerceIn(MIN_SIZE_RATIO, MAX_SIZE_RATIO)
|
|
val safeHeight = heightRatio.sanitize(DEFAULT_SIZE_RATIO).coerceIn(MIN_SIZE_RATIO, MAX_SIZE_RATIO)
|
|
return ScanRegionConfig(
|
|
enabled = enabled,
|
|
widthRatio = safeWidth,
|
|
heightRatio = safeHeight,
|
|
centerXRatio = centerXRatio.sanitize(DEFAULT_CENTER_RATIO).coerceIn(0f, 1f),
|
|
centerYRatio = centerYRatio.sanitize(DEFAULT_CENTER_RATIO).coerceIn(0f, 1f)
|
|
)
|
|
}
|
|
|
|
private fun Float.sanitize(defaultValue: Float): Float {
|
|
return if (isFinite()) this else defaultValue
|
|
}
|
|
}
|
|
|
|
/** 返回扫描框在预览 View 坐标系中的实际位置,越界部分会收敛到 View 内。 */
|
|
fun resolveViewRect(viewWidth: Int, viewHeight: Int): RectF {
|
|
if (viewWidth <= 0 || viewHeight <= 0) return RectF()
|
|
// width / height 都以预览宽度为基准,并使用同一缩放因子收敛,确保始终保持配置比例。
|
|
val desiredWidth = viewWidth * widthRatio
|
|
val desiredHeight = viewWidth * heightRatio
|
|
val sizeScale = min(1f, min(viewWidth / desiredWidth, viewHeight / desiredHeight))
|
|
val frameWidth = desiredWidth * sizeScale
|
|
val frameHeight = desiredHeight * sizeScale
|
|
val wantedLeft = viewWidth * centerXRatio - frameWidth / 2f
|
|
val wantedTop = viewHeight * centerYRatio - frameHeight / 2f
|
|
val left = wantedLeft.coerceIn(0f, max(0f, viewWidth - frameWidth))
|
|
val top = wantedTop.coerceIn(0f, max(0f, viewHeight - frameHeight))
|
|
return RectF(left, top, left + frameWidth, top + frameHeight)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 将 PreviewView 的扫描框映射到 ML Kit 返回 boundingBox 所在的旋转后图像坐标。
|
|
* PreviewView 使用 FILL_CENTER,因此必须补偿等比填充产生的裁剪偏移。
|
|
*/
|
|
object ScanRegionMapper {
|
|
fun mapViewRectToImage(
|
|
viewRect: RectF,
|
|
viewWidth: Int,
|
|
viewHeight: Int,
|
|
imageWidth: Int,
|
|
imageHeight: Int,
|
|
cropRect: Rect,
|
|
rotationDegrees: Int
|
|
): Rect? {
|
|
if (viewRect.isEmpty || viewWidth <= 0 || viewHeight <= 0 || imageWidth <= 0 || imageHeight <= 0) {
|
|
return null
|
|
}
|
|
val visualCrop = YuvFrames.rectToVisualSpace(cropRect, imageWidth, imageHeight, rotationDegrees)
|
|
val cropWidth = visualCrop.width()
|
|
val cropHeight = visualCrop.height()
|
|
if (cropWidth <= 0 || cropHeight <= 0) return null
|
|
|
|
val scale = max(viewWidth.toFloat() / cropWidth, viewHeight.toFloat() / cropHeight)
|
|
if (!scale.isFinite() || scale <= 0f) return null
|
|
val offsetX = (viewWidth - cropWidth * scale) / 2f
|
|
val offsetY = (viewHeight - cropHeight * scale) / 2f
|
|
val mapped = Rect(
|
|
((viewRect.left - offsetX) / scale).toInt() + visualCrop.left,
|
|
((viewRect.top - offsetY) / scale).toInt() + visualCrop.top,
|
|
((viewRect.right - offsetX) / scale).toInt() + visualCrop.left,
|
|
((viewRect.bottom - offsetY) / scale).toInt() + visualCrop.top
|
|
)
|
|
return mapped.apply {
|
|
left = left.coerceIn(visualCrop.left, visualCrop.right)
|
|
top = top.coerceIn(visualCrop.top, visualCrop.bottom)
|
|
right = right.coerceIn(left, visualCrop.right)
|
|
bottom = bottom.coerceIn(top, visualCrop.bottom)
|
|
}.takeIf { it.width() > 0 && it.height() > 0 }
|
|
}
|
|
|
|
fun containsBarcodeCenter(region: Rect, barcodeBox: Rect): Boolean {
|
|
return region.contains(barcodeBox.centerX(), barcodeBox.centerY())
|
|
}
|
|
}
|