完善生产查询与登录权限配置

This commit is contained in:
2026-09-03 19:56:19 +08:00
parent 793f140a45
commit 02c8fb7021
438 changed files with 1595 additions and 573 deletions
@@ -0,0 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="uts.sdk.modules.masCodeScan">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application>
<activity
android:name="uts.sdk.modules.masCodeScan.CodeCaptureActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
</application>
</manifest>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
package uts.sdk.modules.masCodeScan
import java.util.concurrent.ConcurrentLinkedQueue
/**
* 连续扫码命中总线:Activity 入队,index.uts 轮询出队并触发 success。
*/
object CodeScanHitBus {
private val queue = ConcurrentLinkedQueue<Array<String>>()
@JvmStatic
fun detach() {
queue.clear()
}
@JvmStatic
fun emitHit(content: String, format: String, fromAlbum: Boolean) {
queue.offer(
arrayOf(
content,
format,
if (fromAlbum) "1" else "0"
)
)
}
/** 取出一条命中:[content, format, fromAlbumFlag];无数据返回 null */
@JvmStatic
fun pollHit(): Array<String>? {
return queue.poll()
}
}
@@ -0,0 +1,96 @@
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.RectF
import android.graphics.Typeface
import android.view.View
import kotlin.math.min
/**
* 连续扫码成功提示层:覆盖扫描框中部,显示成功状态和本次扫码值。
*/
class ContinuousSuccessOverlayView(
context: Context,
private val successHint: String
) : View(context) {
private val density = resources.displayMetrics.density
private val scaledDensity = resources.displayMetrics.scaledDensity
private val panelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(218, 48, 48, 48)
style = Paint.Style.FILL
}
private val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = 16f * scaledDensity
textAlign = Paint.Align.CENTER
typeface = Typeface.DEFAULT_BOLD
}
private val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = 15f * scaledDensity
textAlign = Paint.Align.CENTER
}
private val panelRect = RectF()
private var frameRect = RectF()
private var stillFrame: Bitmap? = null
private var value = ""
fun showSuccess(scanFrame: RectF, content: String, bitmap: Bitmap?) {
stillFrame?.recycle()
stillFrame = bitmap
frameRect = RectF(scanFrame)
value = content
visibility = VISIBLE
invalidate()
}
fun hideSuccess() {
stillFrame?.recycle()
stillFrame = null
value = ""
visibility = GONE
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (value.isBlank() || width <= 0 || height <= 0) return
stillFrame?.takeIf { !it.isRecycled && it.width > 0 && it.height > 0 }?.let { bitmap ->
val bitmapWidth = bitmap.width.toFloat()
val bitmapHeight = bitmap.height.toFloat()
val scale = maxOf(width / bitmapWidth, height / bitmapHeight)
val drawWidth = bitmapWidth * scale
val drawHeight = bitmapHeight * scale
val left = (width - drawWidth) / 2f
val top = (height - drawHeight) / 2f
canvas.drawBitmap(bitmap, null, RectF(left, top, left + drawWidth, top + drawHeight), null)
}
val availableWidth = if (frameRect.isEmpty) width.toFloat() else frameRect.width()
val panelWidth = min(300f * density, maxOf(0f, availableWidth - 24f * density))
val panelHeight = 68f * density
val centerX = if (frameRect.isEmpty) width / 2f else frameRect.centerX()
val centerY = if (frameRect.isEmpty) height / 2f else frameRect.centerY()
panelRect.set(
centerX - panelWidth / 2f,
centerY - panelHeight / 2f,
centerX + panelWidth / 2f,
centerY + panelHeight / 2f
)
panelRect.left = panelRect.left.coerceAtLeast(12f * density)
panelRect.right = panelRect.right.coerceAtMost(width - 12f * density)
panelRect.top = panelRect.top.coerceAtLeast(24f * density)
panelRect.bottom = panelRect.bottom.coerceAtMost(height - 24f * density)
canvas.drawRoundRect(panelRect, 12f * density, 12f * density, panelPaint)
val titleBaseline = panelRect.centerY() - 3f * density
val valueBaseline = panelRect.centerY() + 19f * density
canvas.drawText(successHint, panelRect.centerX(), titleBaseline, titlePaint)
canvas.drawText(value, panelRect.centerX(), valueBaseline, valuePaint)
}
}
@@ -0,0 +1,267 @@
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)
}
}
@@ -0,0 +1,328 @@
package uts.sdk.modules.masCodeScan
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.BlurMaskFilter
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Path
import android.graphics.Shader
import android.view.View
import kotlin.math.max
import kotlin.math.min
/**
* 扫描框视觉样式。颜色支持 #RRGGBB、#AARRGGBB 和 rgba()。
*/
data class ScanStyleConfig(
val showMask: Boolean = true,
val showBeam: Boolean = true,
val borderColor: Int = Color.rgb(25, 190, 107),
val maskColor: Int = Color.argb(115, 0, 0, 0),
val beamColor: Int = Color.rgb(18, 232, 128),
val beamDuration: Long = 1800L,
val cornerStyle: String = "corner",
val cornerRadius: Float = 20f
) {
companion object {
private const val DEFAULT_BORDER_COLOR = "#19be6b"
private const val DEFAULT_MASK_COLOR = "rgba(0, 0, 0, 0.45)"
private const val DEFAULT_BEAM_COLOR = "#12e880"
private const val DEFAULT_DURATION = 1800L
fun safe(
showMask: Boolean,
showBeam: Boolean,
borderColor: String,
maskColor: String,
beamColor: String,
beamDuration: Int,
cornerStyle: String,
cornerRadius: Int
): ScanStyleConfig {
return ScanStyleConfig(
showMask = showMask,
showBeam = showBeam,
borderColor = parseColor(borderColor, DEFAULT_BORDER_COLOR),
maskColor = parseColor(maskColor, DEFAULT_MASK_COLOR),
beamColor = parseColor(beamColor, DEFAULT_BEAM_COLOR),
beamDuration = beamDuration.toLong().coerceIn(100L, 60000L),
cornerStyle = when (cornerStyle) {
"full" -> "full"
"rounded" -> "rounded"
"rounded-corner" -> "rounded-corner"
else -> "corner"
},
cornerRadius = cornerRadius.toFloat().coerceIn(0f, 160f)
)
}
private fun parseColor(value: String, fallback: String): Int {
val input = value.trim()
if (input.startsWith("rgba(") && input.endsWith(")")) {
val parts = input.substring(5, input.length - 1).split(',').map { it.trim() }
if (parts.size == 4) {
val red = parts[0].toIntOrNull()
val green = parts[1].toIntOrNull()
val blue = parts[2].toIntOrNull()
val alpha = parts[3].toFloatOrNull()
if (red != null && green != null && blue != null && alpha != null) {
return Color.argb(
(alpha.coerceIn(0f, 1f) * 255f).toInt(),
red.coerceIn(0, 255),
green.coerceIn(0, 255),
blue.coerceIn(0, 255)
)
}
}
}
return try {
Color.parseColor(input)
} catch (_: IllegalArgumentException) {
Color.parseColor(fallback)
}
}
}
}
/**
* 取景框光条装饰层。
*/
class ScanFrameDecorView(
context: Context,
private val scanRegion: ScanRegionConfig,
private val scanStyle: ScanStyleConfig,
private val hint: String
) : View(context) {
private val density = resources.displayMetrics.density
private val beamH = 4f * density
private val beamInset = 12f * density
private val cornerLength = 24f * density
private val finderRect = RectF()
private val roundedFinderPath = Path()
private var beamProgress = 0f
private var beamAnimator: ValueAnimator? = null
private val beamPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}
private val regionMaskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = scanStyle.maskColor
style = Paint.Style.FILL
}
private val regionBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = scanStyle.borderColor
style = Paint.Style.STROKE
strokeWidth = 2f * density
strokeCap = Paint.Cap.SQUARE
}
private val beamGlowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(
(Color.alpha(scanStyle.beamColor) * 0.2f).toInt(),
Color.red(scanStyle.beamColor),
Color.green(scanStyle.beamColor),
Color.blue(scanStyle.beamColor)
)
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(18f * density, BlurMaskFilter.Blur.NORMAL)
}
private val hintPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = 14f * resources.displayMetrics.scaledDensity
textAlign = Paint.Align.CENTER
}
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
runBeamLoop()
}
override fun onDetachedFromWindow() {
haltBeamLoop()
super.onDetachedFromWindow()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
layoutFinder(w, h)
updateFinderPath()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (width <= 0 || height <= 0) return
if (scanRegion.enabled) {
if (scanStyle.showMask) {
paintRegionMask(canvas)
}
paintRegionBorder(canvas)
}
if (scanStyle.showBeam) {
paintBeam(canvas)
}
if (hint.isNotBlank()) {
val hintBaseline = min(height - 24f * density, finderRect.bottom + 30f * density)
canvas.drawText(hint, finderRect.centerX(), hintBaseline, hintPaint)
}
}
fun haltBeamLoop() {
beamAnimator?.cancel()
beamAnimator = null
}
private fun runBeamLoop() {
if (beamAnimator != null) return
beamAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = scanStyle.beamDuration
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.RESTART
addUpdateListener {
beamProgress = it.animatedValue as Float
invalidate()
}
start()
}
}
private fun layoutFinder(w: Int, h: Int) {
if (scanRegion.enabled) {
finderRect.set(scanRegion.resolveViewRect(w, h))
return
}
// 未配置 scanRegion 时保持旧版默认扫描框,避免影响现有调用方。
val horizontalMargin = 36f * density
val maxFrameWidth = min(w * 0.74f, 320f * density)
val frameWidth = min(maxFrameWidth, w - horizontalMargin * 2f)
val frameHeight = frameWidth
val left = (w - frameWidth) / 2f
val top = (h - frameHeight) / 2f - 44f * density
finderRect.set(left, max(24f * density, top), left + frameWidth, max(24f * density, top) + frameHeight)
}
private fun updateFinderPath() {
val maxRadius = min(finderRect.width(), finderRect.height()) / 2f
val radius = min(scanStyle.cornerRadius * density, maxRadius)
roundedFinderPath.reset()
roundedFinderPath.addRoundRect(finderRect, radius, radius, Path.Direction.CW)
roundedFinderPath.close()
}
private fun paintRegionMask(canvas: Canvas) {
if (scanStyle.cornerStyle == "rounded" || scanStyle.cornerStyle == "rounded-corner") {
canvas.drawColor(scanStyle.maskColor)
canvas.drawPath(roundedFinderPath, Paint(Paint.ANTI_ALIAS_FLAG).apply {
xfermode = android.graphics.PorterDuffXfermode(android.graphics.PorterDuff.Mode.CLEAR)
})
return
}
canvas.drawRect(0f, 0f, width.toFloat(), finderRect.top, regionMaskPaint)
canvas.drawRect(0f, finderRect.bottom, width.toFloat(), height.toFloat(), regionMaskPaint)
canvas.drawRect(0f, finderRect.top, finderRect.left, finderRect.bottom, regionMaskPaint)
canvas.drawRect(finderRect.right, finderRect.top, width.toFloat(), finderRect.bottom, regionMaskPaint)
}
private fun paintRegionBorder(canvas: Canvas) {
if (scanStyle.cornerStyle == "full") {
canvas.drawRect(finderRect, regionBorderPaint)
return
}
if (scanStyle.cornerStyle == "rounded") {
canvas.drawPath(roundedFinderPath, regionBorderPaint)
return
}
if (scanStyle.cornerStyle == "rounded-corner") {
paintRoundedCorners(canvas)
return
}
val left = finderRect.left
val top = finderRect.top
val right = finderRect.right
val bottom = finderRect.bottom
canvas.drawLine(left, top, left + cornerLength, top, regionBorderPaint)
canvas.drawLine(left, top, left, top + cornerLength, regionBorderPaint)
canvas.drawLine(right - cornerLength, top, right, top, regionBorderPaint)
canvas.drawLine(right, top, right, top + cornerLength, regionBorderPaint)
canvas.drawLine(left, bottom - cornerLength, left, bottom, regionBorderPaint)
canvas.drawLine(left, bottom, left + cornerLength, bottom, regionBorderPaint)
canvas.drawLine(right - cornerLength, bottom, right, bottom, regionBorderPaint)
canvas.drawLine(right, bottom - cornerLength, right, bottom, regionBorderPaint)
}
private fun paintRoundedCorners(canvas: Canvas) {
val radius = min(scanStyle.cornerRadius * density, min(finderRect.width(), finderRect.height()) / 2f)
val left = finderRect.left
val top = finderRect.top
val right = finderRect.right
val bottom = finderRect.bottom
val segmentLength = min(this.cornerLength, radius + this.cornerLength)
val cornerOnlyPath = Path()
val topLeft = RectF(left, top, left + radius * 2f, top + radius * 2f)
val topRight = RectF(right - radius * 2f, top, right, top + radius * 2f)
val bottomLeft = RectF(left, bottom - radius * 2f, left + radius * 2f, bottom)
val bottomRight = RectF(right - radius * 2f, bottom - radius * 2f, right, bottom)
cornerOnlyPath.moveTo(left + segmentLength, top)
cornerOnlyPath.lineTo(left + radius, top)
cornerOnlyPath.arcTo(topLeft, 270f, -90f)
cornerOnlyPath.lineTo(left, top + segmentLength)
cornerOnlyPath.moveTo(right - segmentLength, top)
cornerOnlyPath.lineTo(right - radius, top)
cornerOnlyPath.arcTo(topRight, 270f, 90f)
cornerOnlyPath.lineTo(right, top + segmentLength)
cornerOnlyPath.moveTo(left, bottom - segmentLength)
cornerOnlyPath.lineTo(left, bottom - radius)
cornerOnlyPath.arcTo(bottomLeft, 180f, -90f)
cornerOnlyPath.lineTo(left + segmentLength, bottom)
cornerOnlyPath.moveTo(right, bottom - segmentLength)
cornerOnlyPath.lineTo(right, bottom - radius)
cornerOnlyPath.arcTo(bottomRight, 0f, 90f)
cornerOnlyPath.lineTo(right - segmentLength, bottom)
canvas.drawPath(cornerOnlyPath, regionBorderPaint)
}
private fun paintBeam(canvas: Canvas) {
val availableHeight = finderRect.height() - beamInset * 2f
if (availableHeight <= 0f) return
val y = finderRect.top + beamInset + availableHeight * beamProgress
val lineTop = y - beamH / 2f
val lineBottom = y + beamH / 2f
val lineLeft = finderRect.left + beamInset
val lineRight = finderRect.right - beamInset
if (scanStyle.cornerStyle == "rounded" || scanStyle.cornerStyle == "rounded-corner") {
canvas.save()
canvas.clipPath(roundedFinderPath)
}
beamPaint.shader = LinearGradient(
lineLeft, y, lineRight, y,
intArrayOf(
scanStyle.beamColor and 0x00ffffff,
scanStyle.beamColor,
scanStyle.beamColor and 0x00ffffff
),
floatArrayOf(0f, 0.5f, 1f),
Shader.TileMode.CLAMP
)
canvas.drawRect(
lineLeft, lineTop - 10f * density, lineRight, lineBottom + 10f * density, beamGlowPaint
)
canvas.drawRect(lineLeft, lineTop, lineRight, lineBottom, beamPaint)
if (scanStyle.cornerStyle == "rounded" || scanStyle.cornerStyle == "rounded-corner") {
canvas.restore()
}
}
}
@@ -0,0 +1,108 @@
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())
}
}
@@ -0,0 +1,67 @@
package uts.sdk.modules.masCodeScan
import com.google.mlkit.vision.barcode.common.Barcode
object SymbologyUtil {
fun nameOf(format: Int): String = when (format) {
Barcode.FORMAT_QR_CODE -> "QR_CODE"
Barcode.FORMAT_EAN_13 -> "EAN_13"
Barcode.FORMAT_EAN_8 -> "EAN_8"
Barcode.FORMAT_CODE_128 -> "CODE_128"
Barcode.FORMAT_CODE_39 -> "CODE_39"
Barcode.FORMAT_CODE_93 -> "CODE_93"
Barcode.FORMAT_CODABAR -> "CODABAR"
Barcode.FORMAT_DATA_MATRIX -> "DATA_MATRIX"
Barcode.FORMAT_UPC_A -> "UPC_A"
Barcode.FORMAT_UPC_E -> "UPC_E"
Barcode.FORMAT_ITF -> "ITF"
Barcode.FORMAT_PDF417 -> "PDF417"
Barcode.FORMAT_AZTEC -> "AZTEC"
else -> "UNKNOWN"
}
fun hasSupportedFormat(csv: String?): Boolean {
if (csv.isNullOrBlank()) return true
return csv.split(',').any { tokenToFormat(it.trim().uppercase()) != null }
}
fun parseFormatList(csv: String?): IntArray {
if (csv.isNullOrBlank()) {
return intArrayOf(Barcode.FORMAT_ALL_FORMATS)
}
val mapped = csv.split(',')
.map { it.trim().uppercase() }
.mapNotNull { tokenToFormat(it) }
.distinct()
if (mapped.isEmpty()) {
return intArrayOf(Barcode.FORMAT_ALL_FORMATS)
}
// ALL 与具体码制混用时以全格式为准,避免 ML Kit 参数异常
if (mapped.contains(Barcode.FORMAT_ALL_FORMATS)) {
return intArrayOf(Barcode.FORMAT_ALL_FORMATS)
}
return mapped.toIntArray()
}
private fun tokenToFormat(token: String): Int? = when (token) {
"QR_CODE", "QR" -> Barcode.FORMAT_QR_CODE
"EAN_13" -> Barcode.FORMAT_EAN_13
"EAN_8" -> Barcode.FORMAT_EAN_8
"CODE_128" -> Barcode.FORMAT_CODE_128
"CODE_39" -> Barcode.FORMAT_CODE_39
"CODE_93" -> Barcode.FORMAT_CODE_93
"CODABAR" -> Barcode.FORMAT_CODABAR
"DATA_MATRIX" -> Barcode.FORMAT_DATA_MATRIX
"UPC_A" -> Barcode.FORMAT_UPC_A
"UPC_E" -> Barcode.FORMAT_UPC_E
"ITF" -> Barcode.FORMAT_ITF
"PDF417" -> Barcode.FORMAT_PDF417
"AZTEC" -> Barcode.FORMAT_AZTEC
"ALL", "FORMAT_ALL_FORMATS" -> Barcode.FORMAT_ALL_FORMATS
else -> null
}
fun isDecodedText(barcode: Barcode): Boolean {
return !barcode.rawValue.isNullOrBlank()
}
}
@@ -0,0 +1,136 @@
package uts.sdk.modules.masCodeScan
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import java.io.ByteArrayOutputStream
object YuvFrames {
fun imageToBitmap(
image: Image,
width: Int,
height: Int,
cropRect: Rect,
rotationDegrees: Int
): Bitmap? {
return try {
val planes = image.planes
val yPlane = planes[0]
val uPlane = planes[1]
val vPlane = planes[2]
val yRowStride = yPlane.rowStride
val yPixelStride = yPlane.pixelStride
val uRowStride = uPlane.rowStride
val uPixelStride = uPlane.pixelStride
val vRowStride = vPlane.rowStride
val vPixelStride = vPlane.pixelStride
val nv21Size = width * height * 3 / 2
val nv21 = ByteArray(nv21Size)
var destOffset = 0
if (yPixelStride == 1) {
for (row in 0 until height) {
yPlane.buffer.position(row * yRowStride)
yPlane.buffer.get(nv21, destOffset, width)
destOffset += width
}
} else {
val yBuffer = yPlane.buffer
for (row in 0 until height) {
for (col in 0 until width) {
nv21[destOffset++] = yBuffer.get(row * yRowStride + col * yPixelStride)
}
}
}
var uvOffset = width * height
if (uPixelStride == 1 && vPixelStride == 1) {
for (row in 0 until height / 2) {
vPlane.buffer.position(row * vRowStride)
vPlane.buffer.get(nv21, uvOffset + row * width, width / 2)
}
for (row in 0 until height / 2) {
uPlane.buffer.position(row * uRowStride)
uPlane.buffer.get(nv21, uvOffset + width / 2 + row * width, width / 2)
}
} else {
val uBuffer = uPlane.buffer
val vBuffer = vPlane.buffer
for (row in 0 until height / 2) {
for (col in 0 until width / 2) {
val uvIndex = row * uRowStride + col * uPixelStride
nv21[uvOffset++] = vBuffer.get(uvIndex)
nv21[uvOffset++] = uBuffer.get(uvIndex)
}
}
}
val safeCropRect = Rect(cropRect).apply {
left = left.coerceIn(0, width)
top = top.coerceIn(0, height)
right = right.coerceIn(left, width)
bottom = bottom.coerceIn(top, height)
}
val jpegRect = if (safeCropRect.width() > 0 && safeCropRect.height() > 0) {
safeCropRect
} else {
Rect(0, 0, width, height)
}
val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null)
val baos = ByteArrayOutputStream()
yuvImage.compressToJpeg(jpegRect, 85, baos)
val jpegData = baos.toByteArray()
baos.close()
var bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
if (bitmap != null && rotationDegrees != 0) {
val matrix = Matrix()
matrix.postRotate(rotationDegrees.toFloat())
val rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
if (rotated != bitmap) {
bitmap.recycle()
}
bitmap = rotated
}
bitmap
} catch (_: Exception) {
null
}
}
fun rectToVisualSpace(
rect: Rect,
sourceWidth: Int,
sourceHeight: Int,
rotationDegrees: Int
): Rect {
return when (rotationDegrees) {
90 -> Rect(
sourceHeight - rect.bottom,
rect.left,
sourceHeight - rect.top,
rect.right
)
180 -> Rect(
sourceWidth - rect.right,
sourceHeight - rect.bottom,
sourceWidth - rect.left,
sourceHeight - rect.top
)
270 -> Rect(
rect.top,
sourceWidth - rect.right,
rect.bottom,
sourceWidth - rect.left
)
else -> Rect(rect)
}
}
}
@@ -0,0 +1,12 @@
{
"dependencies": [
"androidx.activity:activity:1.8.0",
"androidx.camera:camera-core:1.4.1",
"androidx.camera:camera-camera2:1.4.1",
"androidx.camera:camera-lifecycle:1.4.1",
"androidx.camera:camera-view:1.4.1",
"androidx.appcompat:appcompat:1.7.0",
"com.google.mlkit:barcode-scanning:17.3.0"
],
"minSdkVersion": 21
}
@@ -0,0 +1,356 @@
import Activity from 'android.app.Activity'
import Intent from 'android.content.Intent'
import { OpenCodeScan, CodeScanErrorCode, CodeScanOptions, CodeScanSuccess } from '../interface.uts'
import { CodeScanFailImpl } from '../unierror.uts'
const LAUNCH_REQ: Int = 53011
const EXTRA_PAYLOAD = 'mas_content'
const EXTRA_FORMAT = 'mas_format'
const EXTRA_TITLE = 'mas_title'
const EXTRA_HINT = 'mas_hint'
const EXTRA_SUCCESS_HINT = 'mas_success_hint'
const EXTRA_EMPTY_HINT = 'mas_empty_hint'
const EXTRA_SMART_ZOOM = 'mas_smart_zoom'
const EXTRA_TORCH_MODE = 'mas_torch_mode'
const EXTRA_RESULT_MODE = 'mas_result_mode'
const EXTRA_SHOW_PAUSE_BUTTON = 'mas_show_pause_button'
const EXTRA_VIBRATE = 'mas_vibrate'
const EXTRA_CONTINUOUS = 'mas_continuous'
const EXTRA_FROM_ALBUM = 'mas_from_album'
const EXTRA_FORMATS = 'mas_formats'
const EXTRA_SCAN_REGION_ENABLED = 'mas_scan_region_enabled'
const EXTRA_SCAN_REGION = 'mas_scan_region'
const EXTRA_SCAN_STYLE_SHOW_MASK = 'mas_scan_style_show_mask'
const EXTRA_SCAN_STYLE_SHOW_BEAM = 'mas_scan_style_show_beam'
const EXTRA_SCAN_STYLE_BORDER_COLOR = 'mas_scan_style_border_color'
const EXTRA_SCAN_STYLE_MASK_COLOR = 'mas_scan_style_mask_color'
const EXTRA_SCAN_STYLE_BEAM_COLOR = 'mas_scan_style_beam_color'
const EXTRA_SCAN_STYLE_BEAM_DURATION = 'mas_scan_style_beam_duration'
const EXTRA_SCAN_STYLE_CORNER = 'mas_scan_style_corner'
const EXTRA_SCAN_STYLE_CORNER_RADIUS = 'mas_scan_style_corner_radius'
const EXTRA_SCAN_TIMEOUT = 'mas_scan_timeout'
const EXTRA_CONTINUOUS_SUCCESS_DURATION = 'mas_continuous_success_duration'
const EXTRA_ERR = 'mas_err'
const EXTRA_FROM_ALBUM_RESULT = 'mas_result_from_album'
const EXTRA_HAD_HITS = 'mas_had_hits'
const CAPTURE_ACTIVITY = 'uts.sdk.modules.masCodeScan.CodeCaptureActivity'
let activityResultHook: ((requestCode: Int, resultCode: Int, data?: Intent) => void) | null = null
let activeOptions: CodeScanOptions | null = null
let continuousHitCount: Int = 0
let pollTimerId: number = -1
let sessionContinuous: boolean = false
let continuousSuccess: ((res: CodeScanSuccess) => void) | null = null
@UTSJS.keepAlive
export function openCodeScan(options?: CodeScanOptions | null): void {
const opts = options != null ? options : null
const host = UTSAndroid.getUniActivity()
if (host == null) {
emitFailure(opts, 9021001)
return
}
launchCapture(host, opts)
}
function clearPollTimer() {
if (pollTimerId >= 0) {
clearInterval(pollTimerId)
pollTimerId = -1
}
}
function drainHits() {
const current = activeOptions
const success = continuousSuccess
if (current == null || success == null) return
while (true) {
const hit = CodeScanHitBus.pollHit()
if (hit == null) break
const content = hit[0]
const format = hit[1]
const fromAlbumFlag = hit[2]
if (content == null || format == null || fromAlbumFlag == null) continue
continuousHitCount++
const ok: CodeScanSuccess = {
content: content,
format: format,
fromAlbum: fromAlbumFlag == '1'
}
success(ok)
}
}
function launchCapture(host: Activity, opts: CodeScanOptions | null): void {
if (activityResultHook != null) {
UTSAndroid.offAppActivityResult(activityResultHook)
activityResultHook = null
}
clearPollTimer()
CodeScanHitBus.detach()
activeOptions = opts
continuousHitCount = 0
sessionContinuous = opts != null && opts.continuous == true
continuousSuccess = sessionContinuous && opts != null ? opts.success : null
if (sessionContinuous) {
pollTimerId = setInterval(() => {
drainHits()
}, 120)
}
const onResult = (requestCode: Int, resultCode: Int, data?: Intent) => {
if (requestCode != LAUNCH_REQ) {
return
}
UTSAndroid.offAppActivityResult()
activityResultHook = null
drainHits()
clearPollTimer()
CodeScanHitBus.detach()
const current = activeOptions
const wasContinuous = sessionContinuous
sessionContinuous = false
if (resultCode != Activity.RESULT_OK) {
let errCode: CodeScanErrorCode = 9021003
if (data != null) {
const nativeErr = data.getIntExtra(EXTRA_ERR, 0)
if (nativeErr >= 9021002 && nativeErr <= 9021011) {
errCode = nativeErr as CodeScanErrorCode
}
}
// 连续扫已有命中:关闭只 complete,不 fail
if (wasContinuous && continuousHitCount > 0) {
if (current != null) {
current.complete?.({ closed: true, hitCount: continuousHitCount })
}
activeOptions = null
continuousSuccess = null
return
}
if (wasContinuous && data != null && data.getBooleanExtra(EXTRA_HAD_HITS, false)) {
if (current != null) {
current.complete?.({ closed: true, hitCount: continuousHitCount })
}
activeOptions = null
continuousSuccess = null
return
}
if (wasContinuous && current != null) {
emitFailure(current, errCode)
const complete = current.complete
if (complete != null) {
complete({ closed: true, hitCount: continuousHitCount, failed: true, errCode: errCode })
}
activeOptions = null
continuousSuccess = null
return
}
emitFailure(current, errCode)
activeOptions = null
continuousSuccess = null
return
}
if (wasContinuous) {
if (current != null) {
current.complete?.({ closed: true, hitCount: continuousHitCount })
}
activeOptions = null
continuousSuccess = null
return
}
let payload = ''
let formatName = ''
let fromAlbum = false
if (data != null) {
const rawPayload = data.getStringExtra(EXTRA_PAYLOAD)
const rawFormat = data.getStringExtra(EXTRA_FORMAT)
payload = rawPayload != null ? rawPayload : ''
formatName = rawFormat != null ? rawFormat : ''
fromAlbum = data.getBooleanExtra(EXTRA_FROM_ALBUM_RESULT, false)
}
if (payload == '') {
emitFailure(current, 9021004)
activeOptions = null
continuousSuccess = null
return
}
const ok: CodeScanSuccess = {
content: payload,
format: formatName,
fromAlbum: fromAlbum
}
if (current != null) {
current.success?.(ok)
current.complete?.(ok)
}
activeOptions = null
continuousSuccess = null
}
activityResultHook = onResult
UTSAndroid.onAppActivityResult(onResult)
let pageTitle = '扫一扫'
let scanHint = '请将条码放入扫描框内'
let successHint = '扫码成功'
let emptyHint = '未识别到有效条码'
let useSmartZoom = true
let useTorchMode = 'off'
let useResultMode = 'immediate'
let useShowPauseButton = false
let useVibrate = true
let useContinuous = false
let useFromAlbum = false
let useScanRegion = false
let regionWidth = 0.74
let regionHeight = 0.74
let regionCenterX = 0.5
let regionCenterY = 0.5
let formatsCsv = ''
let styleShowMask = true
let styleShowBeam = true
let styleBorderColor = '#19be6b'
let styleMaskColor = 'rgba(0, 0, 0, 0.45)'
let styleBeamColor = '#12e880'
let styleBeamDuration = 1800
let styleCorner = 'corner'
let styleCornerRadius = 20
let scanTimeout = 0
let continuousSuccessDuration = 1000
if (opts != null) {
if (opts.title != null && opts.title!.trim().length > 0) {
pageTitle = opts.title!.trim()
}
if (opts.hint != null && opts.hint!.trim().length > 0) {
scanHint = opts.hint!.trim()
}
if (opts.successHint != null && opts.successHint!.trim().length > 0) {
successHint = opts.successHint!.trim()
}
if (opts.emptyHint != null && opts.emptyHint!.trim().length > 0) {
emptyHint = opts.emptyHint!.trim()
}
if (opts.smartZoom != null) {
useSmartZoom = opts.smartZoom!
}
if (opts.torch == 'on' || opts.torch == 'auto' || opts.torch == 'off') {
useTorchMode = opts.torch!
}
if (opts.resultMode == 'confirm' || opts.resultMode == 'pause' || opts.resultMode == 'immediate') {
useResultMode = opts.resultMode!
}
if (opts.showPauseButton != null) {
useShowPauseButton = opts.showPauseButton!
}
if (opts.vibrate != null) {
useVibrate = opts.vibrate!
}
if (opts.continuous != null) {
useContinuous = opts.continuous!
}
if (opts.fromAlbum != null) {
useFromAlbum = opts.fromAlbum!
}
if (opts.scanRegion != null) {
useScanRegion = opts.scanRegion!.fullScreen != true
if (opts.scanRegion!.width != null) {
regionWidth = opts.scanRegion!.width!
}
if (opts.scanRegion!.height != null) {
regionHeight = opts.scanRegion!.height!
}
if (opts.scanRegion!.centerX != null) {
regionCenterX = opts.scanRegion!.centerX!
}
if (opts.scanRegion!.centerY != null) {
regionCenterY = opts.scanRegion!.centerY!
}
}
if (opts.scanTimeout != null) {
scanTimeout = Math.floor(opts.scanTimeout!)
}
if (opts.continuousSuccessDuration != null) {
continuousSuccessDuration = Math.max(0, Math.min(600000, Math.floor(opts.continuousSuccessDuration!)))
}
if (opts.scanStyle != null) {
if (opts.scanStyle!.showMask != null) {
styleShowMask = opts.scanStyle!.showMask!
}
if (opts.scanStyle!.showBeam != null) {
styleShowBeam = opts.scanStyle!.showBeam!
}
if (opts.scanStyle!.borderColor != null) {
styleBorderColor = opts.scanStyle!.borderColor!
}
if (opts.scanStyle!.maskColor != null) {
styleMaskColor = opts.scanStyle!.maskColor!
}
if (opts.scanStyle!.beamColor != null) {
styleBeamColor = opts.scanStyle!.beamColor!
}
if (opts.scanStyle!.beamDuration != null) {
styleBeamDuration = opts.scanStyle!.beamDuration!
}
if (opts.scanStyle!.cornerStyle == 'full' || opts.scanStyle!.cornerStyle == 'corner' || opts.scanStyle!.cornerStyle == 'rounded' || opts.scanStyle!.cornerStyle == 'rounded-corner') {
styleCorner = opts.scanStyle!.cornerStyle!
}
if (opts.scanStyle!.cornerRadius != null) {
styleCornerRadius = Math.max(0, Math.min(160, Math.floor(opts.scanStyle!.cornerRadius!)))
}
}
if (opts.formats != null && opts.formats!.length > 0) {
const parts: string[] = []
for (let i = 0; i < opts.formats!.length; i++) {
const item = opts.formats![i]
if (item != null && item.length > 0) {
parts.push(item)
}
}
formatsCsv = parts.join(',')
}
}
const intent = new Intent()
intent.setClassName(host, CAPTURE_ACTIVITY)
intent.putExtra(EXTRA_TITLE, pageTitle)
intent.putExtra(EXTRA_HINT, scanHint)
intent.putExtra(EXTRA_SUCCESS_HINT, successHint)
intent.putExtra(EXTRA_EMPTY_HINT, emptyHint)
intent.putExtra(EXTRA_SMART_ZOOM, useSmartZoom)
intent.putExtra(EXTRA_TORCH_MODE, useTorchMode)
intent.putExtra(EXTRA_RESULT_MODE, useResultMode)
intent.putExtra(EXTRA_SHOW_PAUSE_BUTTON, useShowPauseButton)
intent.putExtra(EXTRA_VIBRATE, useVibrate)
intent.putExtra(EXTRA_CONTINUOUS, useContinuous)
intent.putExtra(EXTRA_FROM_ALBUM, useFromAlbum)
intent.putExtra(EXTRA_FORMATS, formatsCsv)
intent.putExtra(EXTRA_SCAN_REGION_ENABLED, useScanRegion)
intent.putExtra(EXTRA_SCAN_REGION, `${regionWidth},${regionHeight},${regionCenterX},${regionCenterY}`)
intent.putExtra(EXTRA_SCAN_STYLE_SHOW_MASK, styleShowMask)
intent.putExtra(EXTRA_SCAN_STYLE_SHOW_BEAM, styleShowBeam)
intent.putExtra(EXTRA_SCAN_STYLE_BORDER_COLOR, styleBorderColor)
intent.putExtra(EXTRA_SCAN_STYLE_MASK_COLOR, styleMaskColor)
intent.putExtra(EXTRA_SCAN_STYLE_BEAM_COLOR, styleBeamColor)
intent.putExtra(EXTRA_SCAN_STYLE_BEAM_DURATION, styleBeamDuration)
intent.putExtra(EXTRA_SCAN_STYLE_CORNER, styleCorner)
intent.putExtra(EXTRA_SCAN_STYLE_CORNER_RADIUS, styleCornerRadius)
intent.putExtra(EXTRA_SCAN_TIMEOUT, scanTimeout)
intent.putExtra(EXTRA_CONTINUOUS_SUCCESS_DURATION, continuousSuccessDuration)
host.startActivityForResult(intent, LAUNCH_REQ)
}
function emitFailure(opts: CodeScanOptions | null, code: CodeScanErrorCode): void {
const err = new CodeScanFailImpl(code)
if (opts != null) {
opts.fail?.(err)
opts.complete?.(err)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB