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 = emptyList() private var hitMarks: List = 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, 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) } }