Files
MrleeAdmin/frontend/app/uni_modules/mas-code-scan/utssdk/app-android/MultiCodePickView.kt
T

268 lines
9.5 KiB
Kotlin

package uts.sdk.modules.masCodeScan
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.os.SystemClock
import android.view.View
import com.google.mlkit.vision.barcode.common.Barcode
import kotlin.math.max
import kotlin.math.sin
data class HitMark(
val rawValue: String,
val format: Int,
val formatName: String,
val screenRect: Rect
) {
val centerX: Float get() = screenRect.exactCenterX()
val centerY: Float get() = screenRect.exactCenterY()
}
/**
* 多码点选覆盖层:定格帧 + 圆点标记。
*/
class MultiCodePickView(context: Context) : View(context) {
private var stillFrame: Bitmap? = null
private var rawHits: List<Barcode> = emptyList()
private var hitMarks: List<HitMark> = emptyList()
private var visualCrop: Rect = Rect()
private var stillW: Int = 0
private var stillH: Int = 0
private var layerW: Int = 0
private var layerH: Int = 0
private var recognizing: Boolean = false
private var recognizingStartedAt: Long = 0L
private val density: Float = resources.displayMetrics.density
private val dotOuterR: Float = 18f * density
private val dotInnerR: Float = 6f * density
private val touchR: Float = 28f * density
private val breatheDurationMs = 1500L
private val haloPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = Color.argb(60, 7, 193, 96)
}
private val shellPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = Color.WHITE
}
private val corePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = Color.rgb(7, 193, 96)
}
private val shadePaint = Paint().apply {
color = Color.argb(40, 0, 0, 0)
}
private val promptBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(190, 0, 0, 0)
}
private val promptTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = 16f * resources.displayMetrics.scaledDensity
textAlign = Paint.Align.CENTER
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
private val loadingTrackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 3f * density
strokeCap = Paint.Cap.ROUND
color = Color.argb(100, 255, 255, 255)
}
private val loadingArcPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 3f * density
strokeCap = Paint.Cap.ROUND
color = Color.rgb(7, 193, 96)
}
private val loadingTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = 15f * resources.displayMetrics.scaledDensity
textAlign = Paint.Align.CENTER
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
layerW = right - left
layerH = bottom - top
if (changed && rawHits.isNotEmpty()) {
rebuildHitMarks()
}
}
fun showRecognizing(bitmap: Bitmap) {
if (stillFrame !== bitmap) {
stillFrame?.recycle()
}
stillFrame = bitmap
rawHits = emptyList()
hitMarks = emptyList()
visualCrop.setEmpty()
stillW = bitmap.width
stillH = bitmap.height
recognizing = true
recognizingStartedAt = SystemClock.elapsedRealtime()
visibility = VISIBLE
invalidate()
}
fun presentStillFrame(bitmap: Bitmap, barcodes: List<Barcode>, visualCropRect: Rect) {
if (stillFrame !== bitmap) {
stillFrame?.recycle()
}
stillFrame = bitmap
rawHits = barcodes
visualCrop = Rect(visualCropRect)
stillW = bitmap.width
stillH = bitmap.height
recognizing = false
rebuildHitMarks()
visibility = VISIBLE
invalidate()
}
private fun rebuildHitMarks() {
if (layerW <= 0 || layerH <= 0 || stillW <= 0 || stillH <= 0) {
hitMarks = emptyList()
return
}
hitMarks = rawHits.mapNotNull { barcode ->
val box = barcode.boundingBox ?: return@mapNotNull null
val adjustedBox = Rect(
box.left - visualCrop.left,
box.top - visualCrop.top,
box.right - visualCrop.left,
box.bottom - visualCrop.top
)
val clippedBox = Rect(adjustedBox)
if (!clippedBox.intersect(0, 0, stillW, stillH)) {
return@mapNotNull null
}
val screenRect = mapBoxToScreen(clippedBox, stillW, stillH, layerW, layerH)
HitMark(
rawValue = barcode.rawValue ?: "",
format = barcode.format,
formatName = SymbologyUtil.nameOf(barcode.format),
screenRect = screenRect
)
}
}
fun clear() {
stillFrame?.recycle()
stillFrame = null
rawHits = emptyList()
hitMarks = emptyList()
visualCrop.setEmpty()
stillW = 0
stillH = 0
recognizing = false
recognizingStartedAt = 0L
visibility = GONE
invalidate()
}
fun pickHitAt(x: Float, y: Float): HitMark? {
return hitMarks.minByOrNull {
val dx = x - it.centerX
val dy = y - it.centerY
dx * dx + dy * dy
}?.takeIf {
val dx = x - it.centerX
val dy = y - it.centerY
dx * dx + dy * dy <= touchR * touchR
}
}
private fun mapBoxToScreen(
boundingBox: Rect,
sourceW: Int, sourceH: Int,
viewW: Int, viewH: Int
): Rect {
val scale = max(viewW.toFloat() / sourceW, viewH.toFloat() / sourceH)
val offsetX = (viewW - sourceW * scale) / 2f
val offsetY = (viewH - sourceH * scale) / 2f
return Rect(
(boundingBox.left * scale + offsetX).toInt(),
(boundingBox.top * scale + offsetY).toInt(),
(boundingBox.right * scale + offsetX).toInt(),
(boundingBox.bottom * scale + offsetY).toInt()
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
stillFrame?.let { bmp ->
if (layerW <= 0 || layerH <= 0) return@let
val bmpW = bmp.width.toFloat()
val bmpH = bmp.height.toFloat()
val scale = max(layerW.toFloat() / bmpW, layerH.toFloat() / bmpH)
val drawW = (bmpW * scale).toInt()
val drawH = (bmpH * scale).toInt()
val left = (layerW - drawW) / 2
val top = (layerH - drawH) / 2
val destRect = Rect(left, top, left + drawW, top + drawH)
canvas.drawBitmap(bmp, null, destRect, null)
if (!recognizing) {
canvas.drawRect(destRect, shadePaint)
}
}
if (recognizing) {
drawRecognizing(canvas)
postInvalidateDelayed(16)
return
}
if (hitMarks.size >= 2) {
val prompt = "已识别到多个码,请选择一个"
val horizontalPadding = 18f * density
val verticalPadding = 10f * density
val textBounds = Rect()
promptTextPaint.getTextBounds(prompt, 0, prompt.length, textBounds)
val promptWidth = textBounds.width() + horizontalPadding * 2
val promptHeight = textBounds.height() + verticalPadding * 2
val promptLeft = (layerW - promptWidth) / 2f
// 放在底部操作按钮上方,避免遮挡条码和顶部关闭按钮。
val promptTop = layerH - promptHeight - 100f * density
canvas.drawRoundRect(
promptLeft,
promptTop,
promptLeft + promptWidth,
promptTop + promptHeight,
promptHeight / 2,
promptHeight / 2,
promptBackgroundPaint
)
val baseline = promptTop + verticalPadding - textBounds.top
canvas.drawText(prompt, layerW / 2f, baseline, promptTextPaint)
}
val breatheProgress = (SystemClock.elapsedRealtime() % breatheDurationMs).toFloat() / breatheDurationMs
val breatheWave = ((sin(breatheProgress * Math.PI * 2.0) + 1.0) / 2.0).toFloat()
for (mark in hitMarks) {
haloPaint.alpha = (34 + breatheWave * 70).toInt()
canvas.drawCircle(mark.centerX, mark.centerY, touchR + breatheWave * 8f * density, haloPaint)
canvas.drawCircle(mark.centerX, mark.centerY, dotOuterR, shellPaint)
canvas.drawCircle(mark.centerX, mark.centerY, dotInnerR, corePaint)
}
if (hitMarks.isNotEmpty()) {
postInvalidateDelayed(16)
}
}
private fun drawRecognizing(canvas: Canvas) {
val cx = layerW / 2f
val cy = layerH / 2f
val radius = 24f * density
val bounds = android.graphics.RectF(cx - radius, cy - radius, cx + radius, cy + radius)
val elapsed = SystemClock.elapsedRealtime() - recognizingStartedAt
val startAngle = (elapsed % 1000L) * 360f / 1000f - 90f
canvas.drawCircle(cx, cy, radius, loadingTrackPaint)
canvas.drawArc(bounds, startAngle, 110f, false, loadingArcPaint)
canvas.drawText("识别中", cx, cy + radius + 30f * density, loadingTextPaint)
}
}