beautified UI

This commit is contained in:
2026-08-07 22:35:48 +08:00
parent c79c34895c
commit 50b6fac08d
28 changed files with 548 additions and 129 deletions

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Outline
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.graphics.Typeface import android.graphics.Typeface
@@ -14,9 +15,11 @@ import android.util.Size
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewOutlineProvider
import android.widget.EditText import android.widget.EditText
import android.widget.Button import android.widget.Button
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@@ -143,27 +146,116 @@ class MainActivity : AppCompatActivity() {
cameraExecutor.shutdown() cameraExecutor.shutdown()
} }
private fun dp(value: Int): Int {
return (value * resources.displayMetrics.density).toInt()
}
private fun primaryButton(text: String, onClick: () -> Unit): Button {
return Button(this).apply {
this.text = text
textSize = 16f
setTextColor(0xFFFFFFFF.toInt())
typeface = Typeface.DEFAULT_BOLD
isAllCaps = false
setBackgroundResource(R.drawable.bg_button_primary)
minHeight = dp(48)
setPadding(dp(22), 0, dp(22), 0)
setOnClickListener { onClick() }
}
}
private fun secondaryButton(text: String, onClick: () -> Unit): Button {
return Button(this).apply {
this.text = text
textSize = 16f
setTextColor(0xFF1D4ED8.toInt())
typeface = Typeface.DEFAULT_BOLD
isAllCaps = false
setBackgroundResource(R.drawable.bg_button_secondary)
minHeight = dp(48)
setPadding(dp(22), 0, dp(22), 0)
setOnClickListener { onClick() }
}
}
private fun cardButton(iconRes: Int, title: String, subtitle: String, onClick: () -> Unit): View {
return LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setBackgroundResource(R.drawable.bg_card_click)
isClickable = true
isFocusable = true
elevation = dp(3).toFloat()
setPadding(dp(20), dp(18), dp(20), dp(18))
setOnClickListener { onClick() }
addView(FrameLayout(this@MainActivity).apply {
layoutParams = LinearLayout.LayoutParams(dp(52), dp(52))
setBackgroundResource(R.drawable.bg_button_primary)
addView(ImageView(this@MainActivity).apply {
layoutParams = FrameLayout.LayoutParams(dp(28), dp(28), Gravity.CENTER)
setImageResource(iconRes)
})
})
addView(LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
setPadding(dp(16), 0, 0, 0)
addView(TextView(this@MainActivity).apply {
text = title
textSize = 18f
typeface = Typeface.DEFAULT_BOLD
setTextColor(0xFF0F172A.toInt())
})
addView(TextView(this@MainActivity).apply {
text = subtitle
textSize = 13f
setTextColor(0xFF64748B.toInt())
setPadding(0, dp(4), 0, 0)
})
})
addView(TextView(this@MainActivity).apply {
text = ""
textSize = 26f
setTextColor(0xFF94A3B8.toInt())
})
}
}
private fun createContentView(): View { private fun createContentView(): View {
val root = LinearLayout(this).apply { val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setBackgroundColor(0xFFF8FAFC.toInt()) setBackgroundColor(0xFFF1F5F9.toInt())
} }
val header = LinearLayout(this).apply { val header = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setPadding(32, 28, 32, 20) setBackgroundResource(R.drawable.bg_header)
setPadding(dp(24), dp(58), dp(24), dp(18))
} }
header.addView(TextView(this).apply { header.addView(TextView(this).apply {
text = "学生人脸识别" text = "学生人脸识别"
textSize = 24f textSize = 22f
setTextColor(0xFF0F172A.toInt()) typeface = Typeface.DEFAULT_BOLD
setTextColor(0xFFFFFFFF.toInt())
}) })
countText = TextView(this).apply { countText = TextView(this).apply {
text = "已入库 0 名学生" text = "已入库 0 名学生"
textSize = 15f textSize = 13f
setTextColor(0xFF475569.toInt()) setTextColor(0xFFFFFFFF.toInt())
setBackgroundResource(R.drawable.bg_chip_white)
setPadding(dp(12), dp(6), dp(12), dp(6))
} }
header.addView(countText) header.addView(
countText,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = dp(10) }
)
val content = FrameLayout(this).apply { val content = FrameLayout(this).apply {
layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
@@ -192,66 +284,83 @@ class MainActivity : AppCompatActivity() {
private fun createHomePanel(): View { private fun createHomePanel(): View {
return LinearLayout(this).apply { return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER gravity = Gravity.CENTER_HORIZONTAL
setPadding(40, 40, 40, 40) setPadding(dp(24), dp(36), dp(24), dp(36))
addView(TextView(this@MainActivity).apply { addView(TextView(this@MainActivity).apply {
text = "请选择要进入的功能" text = "请选择要进入的功能"
textSize = 26f textSize = 22f
typeface = Typeface.DEFAULT_BOLD typeface = Typeface.DEFAULT_BOLD
setTextColor(0xFF0F172A.toInt()) setTextColor(0xFF0F172A.toInt())
gravity = Gravity.CENTER gravity = Gravity.CENTER
}) })
addView(TextView(this@MainActivity).apply { addView(TextView(this@MainActivity).apply {
text = "识别注册上传分开运行,减少同时占用相机模型和本地服务带来的卡顿。" text = "识别注册上传分开运行,避免同时占用相机模型导致卡顿。"
textSize = 16f textSize = 14f
setTextColor(0xFF475569.toInt()) setTextColor(0xFF64748B.toInt())
gravity = Gravity.CENTER gravity = Gravity.CENTER
setPadding(0, 20, 0, 36) setPadding(0, dp(10), 0, dp(28))
}) })
addView(Button(this@MainActivity).apply { addView(
text = "进入识别" cardButton(
textSize = 20f R.drawable.ic_face_scan,
setPadding(36, 24, 36, 24) "进入识别",
setOnClickListener { switchMode(AppMode.RECOGNITION) } "打开摄像头,实时识别学生身份"
}) ) { switchMode(AppMode.RECOGNITION) },
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = dp(16) }
)
addView(Button(this@MainActivity).apply { addView(
text = "进入注册上传" cardButton(
textSize = 20f R.drawable.ic_upload,
setPadding(36, 24, 36, 24) "进入注册上传",
setOnClickListener { switchMode(AppMode.UPLOAD) } "上传学生照片或视频,更新人脸库"
}) ) { switchMode(AppMode.UPLOAD) },
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
)
} }
} }
private fun createRecognitionPanel(): View { private fun createRecognitionPanel(): View {
return LinearLayout(this).apply { return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setPadding(24, 16, 24, 24) setPadding(dp(16), dp(12), dp(16), dp(16))
recognitionStatusText = TextView(this@MainActivity).apply { recognitionStatusText = TextView(this@MainActivity).apply {
text = "准备进入识别模式" text = "准备进入识别模式"
textSize = 22f textSize = 18f
typeface = Typeface.DEFAULT_BOLD typeface = Typeface.DEFAULT_BOLD
setTextColor(0xFF0F172A.toInt()) setTextColor(0xFF0F172A.toInt())
setPadding(8, 8, 8, 20) setPadding(dp(8), dp(4), dp(8), dp(12))
} }
addView(recognitionStatusText) addView(recognitionStatusText)
val matcherConfigPanel = LinearLayout(this@MainActivity).apply { val matcherConfigPanel = LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setPadding(16, 16, 16, 16) setPadding(dp(16), dp(14), dp(16), dp(14))
setBackgroundColor(0xFFF1F5F9.toInt()) setBackgroundResource(R.drawable.bg_panel_light)
} }
matcherConfigSummaryText = TextView(this@MainActivity).apply { matcherConfigPanel.addView(TextView(this@MainActivity).apply {
text = "识别参数"
textSize = 14f textSize = 14f
setTextColor(0xFF475569.toInt()) typeface = Typeface.DEFAULT_BOLD
setTextColor(0xFF0F172A.toInt())
})
matcherConfigSummaryText = TextView(this@MainActivity).apply {
textSize = 12f
setTextColor(0xFF64748B.toInt())
text = matcherConfigSummary() text = matcherConfigSummary()
setPadding(0, 0, 0, 12) setPadding(0, dp(2), 0, dp(10))
} }
matcherConfigPanel.addView(matcherConfigSummaryText) matcherConfigPanel.addView(matcherConfigSummaryText)
@@ -262,7 +371,7 @@ class MainActivity : AppCompatActivity() {
val euclideanColumn = LinearLayout(this@MainActivity).apply { val euclideanColumn = LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
setPadding(0, 0, 12, 0) setPadding(0, 0, dp(8), 0)
} }
euclideanColumn.addView(TextView(this@MainActivity).apply { euclideanColumn.addView(TextView(this@MainActivity).apply {
text = "欧氏阈值" text = "欧氏阈值"
@@ -273,15 +382,25 @@ class MainActivity : AppCompatActivity() {
setText(formatThreshold(euclideanThreshold)) setText(formatThreshold(euclideanThreshold))
hint = "0.00" hint = "0.00"
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
textSize = 15f
setTextColor(0xFF0F172A.toInt()) setTextColor(0xFF0F172A.toInt())
setBackgroundColor(0xFFFFFFFF.toInt()) setBackgroundResource(R.drawable.bg_input)
setPadding(20, 18, 20, 18) setPadding(dp(12), 0, dp(12), 0)
minHeight = dp(44)
} }
euclideanColumn.addView(euclideanThresholdInput) euclideanColumn.addView(
euclideanThresholdInput,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = dp(6) }
)
thresholdRow.addView(euclideanColumn)
val cosineColumn = LinearLayout(this@MainActivity).apply { val cosineColumn = LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
setPadding(dp(8), 0, 0, 0)
} }
cosineColumn.addView(TextView(this@MainActivity).apply { cosineColumn.addView(TextView(this@MainActivity).apply {
text = "余弦阈值" text = "余弦阈值"
@@ -291,26 +410,37 @@ class MainActivity : AppCompatActivity() {
cosineThresholdInput = EditText(this@MainActivity).apply { cosineThresholdInput = EditText(this@MainActivity).apply {
setText(formatThreshold(cosineThreshold)) setText(formatThreshold(cosineThreshold))
hint = "0.00" hint = "0.00"
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
textSize = 15f
setTextColor(0xFF0F172A.toInt()) setTextColor(0xFF0F172A.toInt())
setBackgroundColor(0xFFFFFFFF.toInt()) setBackgroundResource(R.drawable.bg_input)
setPadding(20, 18, 20, 18) setPadding(dp(12), 0, dp(12), 0)
minHeight = dp(44)
} }
cosineColumn.addView(cosineThresholdInput) cosineColumn.addView(
cosineThresholdInput,
thresholdRow.addView(euclideanColumn) LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = dp(6) }
)
thresholdRow.addView(cosineColumn) thresholdRow.addView(cosineColumn)
matcherConfigPanel.addView(thresholdRow) matcherConfigPanel.addView(thresholdRow)
matcherConfigPanel.addView(Button(this@MainActivity).apply { matcherConfigPanel.addView(LinearLayout(this@MainActivity).apply {
text = "保存识别参数" orientation = LinearLayout.HORIZONTAL
setOnClickListener { saveMatcherConfig() } setPadding(0, dp(12), 0, 0)
})
matcherConfigPanel.addView(Button(this@MainActivity).apply {
text = "自动校准阈值"
setOnClickListener { applySuggestedThresholds() }
})
addView(
secondaryButton("自动校准阈值") { applySuggestedThresholds() },
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
.apply { rightMargin = dp(8) }
)
addView(
primaryButton("保存识别参数") { saveMatcherConfig() },
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
)
})
addView(matcherConfigPanel) addView(matcherConfigPanel)
val cameraFrame = FrameLayout(this@MainActivity).apply { val cameraFrame = FrameLayout(this@MainActivity).apply {
@@ -319,39 +449,51 @@ class MainActivity : AppCompatActivity() {
0, 0,
1f 1f
) )
setBackgroundResource(R.drawable.bg_camera_frame)
setPadding(dp(4), dp(4), dp(4), dp(4))
} }
cameraFrame.addView(
FrameLayout(this@MainActivity).apply {
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, dp(16).toFloat())
}
}
previewView = PreviewView(this@MainActivity).apply { previewView = PreviewView(this@MainActivity).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER scaleType = PreviewView.ScaleType.FILL_CENTER
implementationMode = PreviewView.ImplementationMode.COMPATIBLE implementationMode = PreviewView.ImplementationMode.COMPATIBLE
} }
overlayView = OverlayView(this@MainActivity) overlayView = OverlayView(this@MainActivity)
cameraFrame.addView(previewView, FrameLayout.LayoutParams(-1, -1)) addView(previewView, FrameLayout.LayoutParams(-1, -1))
cameraFrame.addView(overlayView, FrameLayout.LayoutParams(-1, -1)) addView(overlayView, FrameLayout.LayoutParams(-1, -1))
},
FrameLayout.LayoutParams(-1, -1)
)
addView(cameraFrame) addView(cameraFrame)
addView(LinearLayout(this@MainActivity).apply { addView(LinearLayout(this@MainActivity).apply {
gravity = Gravity.CENTER gravity = Gravity.CENTER
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
setPadding(12, 20, 12, 8) setPadding(dp(12), dp(16), dp(12), 0)
addView(Button(this@MainActivity).apply { addView(
text = "返回首页" secondaryButton("返回首页") { switchMode(AppMode.HOME) },
setOnClickListener { switchMode(AppMode.HOME) } LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}) .apply { rightMargin = dp(10) }
)
pauseRecognitionButton = Button(this@MainActivity).apply { pauseRecognitionButton = primaryButton("暂停识别") {
text = "暂停识别"
setOnClickListener {
recognitionEnabled = !recognitionEnabled recognitionEnabled = !recognitionEnabled
text = if (recognitionEnabled) "暂停识别" else "继续识别" pauseRecognitionButton.text = if (recognitionEnabled) "暂停识别" else "继续识别"
recognitionStatusText.text = if (recognitionEnabled) "正在识别" else "识别已暂停" recognitionStatusText.text = if (recognitionEnabled) "正在识别" else "识别已暂停"
if (!recognitionEnabled) { if (!recognitionEnabled) {
overlayView.update(emptyList()) overlayView.update(emptyList())
} }
updateAnalyzerState() updateAnalyzerState()
} }
} addView(pauseRecognitionButton, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
addView(pauseRecognitionButton)
}) })
} }
} }
@@ -360,11 +502,11 @@ class MainActivity : AppCompatActivity() {
return LinearLayout(this).apply { return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER_HORIZONTAL gravity = Gravity.CENTER_HORIZONTAL
setPadding(40, 48, 40, 48) setPadding(dp(24), dp(32), dp(24), dp(32))
uploadStatusText = TextView(this@MainActivity).apply { uploadStatusText = TextView(this@MainActivity).apply {
text = "准备进入注册上传模式" text = "准备进入注册上传模式"
textSize = 24f textSize = 20f
typeface = Typeface.DEFAULT_BOLD typeface = Typeface.DEFAULT_BOLD
setTextColor(0xFF0F172A.toInt()) setTextColor(0xFF0F172A.toInt())
gravity = Gravity.CENTER gravity = Gravity.CENTER
@@ -373,38 +515,62 @@ class MainActivity : AppCompatActivity() {
addView(TextView(this@MainActivity).apply { addView(TextView(this@MainActivity).apply {
setText(R.string.upload_instructions) setText(R.string.upload_instructions)
textSize = 16f textSize = 14f
setTextColor(0xFF475569.toInt()) setTextColor(0xFF64748B.toInt())
gravity = Gravity.CENTER gravity = Gravity.CENTER
setPadding(0, 16, 0, 20) setPadding(0, dp(10), 0, dp(24))
}) })
addView(
LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL
setBackgroundResource(R.drawable.bg_card)
elevation = dp(3).toFloat()
setPadding(dp(18), dp(16), dp(18), dp(16))
addView(TextView(this@MainActivity).apply {
text = "上传地址"
textSize = 12f
setTextColor(0xFF64748B.toInt())
})
uploadUrlText = TextView(this@MainActivity).apply { uploadUrlText = TextView(this@MainActivity).apply {
text = "-" text = "-"
textSize = 20f textSize = 18f
typeface = Typeface.MONOSPACE typeface = Typeface.MONOSPACE
setTextColor(0xFF0F172A.toInt()) setTextColor(0xFF0F172A.toInt())
gravity = Gravity.CENTER gravity = Gravity.CENTER
setPadding(24, 20, 24, 20) setPadding(dp(12), dp(12), dp(12), dp(12))
setTextIsSelectable(true) setTextIsSelectable(true)
setBackgroundColor(0xFFE2E8F0.toInt()) setBackgroundResource(R.drawable.bg_input)
} }
addView(uploadUrlText) addView(
uploadUrlText,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { topMargin = dp(8) }
)
},
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
)
addView(LinearLayout(this@MainActivity).apply { addView(LinearLayout(this@MainActivity).apply {
gravity = Gravity.CENTER gravity = Gravity.CENTER
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
setPadding(12, 28, 12, 8) setPadding(0, dp(24), 0, 0)
addView(Button(this@MainActivity).apply { addView(
text = "刷新地址" secondaryButton("刷新地址") { startUploadSession() },
setOnClickListener { startUploadSession() } LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}) .apply { rightMargin = dp(10) }
)
addView(Button(this@MainActivity).apply { addView(
text = "返回首页" primaryButton("返回首页") { switchMode(AppMode.HOME) },
setOnClickListener { switchMode(AppMode.HOME) } LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}) )
}) })
} }
} }

View File

@@ -20,10 +20,16 @@ class OverlayView @JvmOverloads constructor(
attrs: AttributeSet? = null attrs: AttributeSet? = null
) : View(context, attrs) { ) : View(context, attrs) {
// 识别框外发光画笔
private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 14f
}
// 识别框画笔 // 识别框画笔
private val boxPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val boxPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 6f strokeWidth = 5f
} }
// 标签背景画笔 // 标签背景画笔
@@ -31,22 +37,29 @@ class OverlayView @JvmOverloads constructor(
style = Paint.Style.FILL style = Paint.Style.FILL
} }
// 标签投影画笔
private val labelShadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = Color.argb(70, 0, 0, 0)
}
// 标签文字画笔 // 标签文字画笔
private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE color = Color.WHITE
textSize = 48f textSize = 40f
typeface = Typeface.DEFAULT_BOLD typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
} }
// 置信度文字画笔 // 置信度文字画笔
private val confidencePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val confidencePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE color = Color.WHITE
textSize = 32f textSize = 30f
typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)
} }
// 颜色定义 // 颜色定义
private val colorMatch = Color.rgb(34, 197, 94) // 绿色 - 匹配成功 private val colorMatch = Color.rgb(34, 197, 94) // 绿色 - 匹配成功
private val colorUnsure = Color.rgb(234, 179, 8) // 色 - 不确定 private val colorUnsure = Color.rgb(245, 158, 11) // 色 - 不确定
private val colorNoMatch = Color.rgb(239, 68, 68) // 红色 - 未匹配 private val colorNoMatch = Color.rgb(239, 68, 68) // 红色 - 未匹配
private val colorUnknown = Color.rgb(59, 130, 246) // 蓝色 - 识别中 private val colorUnknown = Color.rgb(59, 130, 246) // 蓝色 - 识别中
@@ -72,43 +85,74 @@ class OverlayView @JvmOverloads constructor(
val boxColor = getBoxColor(item.result) val boxColor = getBoxColor(item.result)
boxPaint.color = boxColor boxPaint.color = boxColor
// 绘制识别框 // 外发光(半透明描边)
canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom, boxPaint) glowPaint.color = boxColor and 0x00FFFFFF or 0x4D000000.toInt()
canvas.drawRoundRect(rect, 14f, 14f, glowPaint)
// 绘制识别框(圆角)
canvas.drawRoundRect(rect, 14f, 14f, boxPaint)
// 绘制标签背景 // 绘制标签背景
val label = item.label val label = item.label
val labelWidth = labelPaint.measureText(label) val labelWidth = labelPaint.measureText(label)
val labelHeight = 60f val labelHeight = 52f
val padding = 12f val padding = 14f
// 标签在框上方 // 标签在框上方
val labelLeft = rect.left + padding val labelLeft = rect.left + 4f
val labelTop = (rect.top - labelHeight - padding).coerceAtLeast(padding) val labelTop = (rect.top - labelHeight - 10f).coerceAtLeast(padding)
val labelRight = labelLeft + labelWidth + padding * 2 val labelRight = labelLeft + labelWidth + padding * 2
val labelBottom = labelTop + labelHeight val labelBottom = labelTop + labelHeight
// 标签背景颜色与框颜色一致 // 标签投影
canvas.drawRoundRect(
labelLeft,
labelTop + 3f,
labelRight,
labelBottom + 3f,
labelHeight / 2f,
labelHeight / 2f,
labelShadowPaint
)
// 标签背景颜色与框颜色一致(胶囊形)
labelBackgroundPaint.color = boxColor labelBackgroundPaint.color = boxColor
canvas.drawRoundRect(labelLeft, labelTop, labelRight, labelBottom, 8f, 8f, labelBackgroundPaint) canvas.drawRoundRect(
labelLeft,
labelTop,
labelRight,
labelBottom,
labelHeight / 2f,
labelHeight / 2f,
labelBackgroundPaint
)
// 绘制标签文字 // 绘制标签文字
canvas.drawText(label, labelLeft + padding, labelTop + 38f, labelPaint) canvas.drawText(label, labelLeft + padding, labelTop + 36f, labelPaint)
// 如果有置信度信息,显示在标签右侧 // 如果有置信度信息,显示在标签右侧
if (item.confidence != null) { if (item.confidence != null) {
val confidenceText = "${(item.confidence * 100).toInt()}%" val confidenceText = "${(item.confidence * 100).toInt()}%"
val confidenceWidth = confidencePaint.measureText(confidenceText) val confidenceWidth = confidencePaint.measureText(confidenceText)
val confLeft = labelRight + 8f val chipLeft = labelRight + 8f
val confTop = labelTop + 8f val chipTop = labelTop + 6f
val confRight = confLeft + confidenceWidth + 16f val chipRight = chipLeft + confidenceWidth + 22f
val confBottom = confTop + 44f val chipBottom = chipTop + 40f
// 置信度背景(半透明黑色 // 置信度背景(深色半透明胶囊
labelBackgroundPaint.color = Color.argb(180, 0, 0, 0) labelBackgroundPaint.color = Color.argb(210, 15, 23, 42)
canvas.drawRoundRect(confLeft, confTop, confRight, confBottom, 6f, 6f, labelBackgroundPaint) canvas.drawRoundRect(
chipLeft,
chipTop,
chipRight,
chipBottom,
20f,
20f,
labelBackgroundPaint
)
// 置信度文字 // 置信度文字
canvas.drawText(confidenceText, confLeft + 8f, confTop + 30f, confidencePaint) canvas.drawText(confidenceText, chipLeft + 11f, chipTop + 27f, confidencePaint)
} }
} }

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#33FFFFFF">
<item>
<shape>
<gradient
android:angle="270"
android:startColor="#2E6BFF"
android:endColor="#1D4ED8" />
<corners android:radius="16dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#1A2563EB">
<item>
<shape>
<solid android:color="#FFFFFF" />
<corners android:radius="16dp" />
<stroke
android:width="1dp"
android:color="#CBD5E1" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<corners android:radius="20dp" />
<stroke
android:width="1dp"
android:color="#E2E8F0" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<corners android:radius="20dp" />
<stroke
android:width="1dp"
android:color="#E2E8F0" />
</shape>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#1A2563EB">
<item>
<shape>
<solid android:color="#FFFFFF" />
<corners android:radius="20dp" />
<stroke
android:width="1dp"
android:color="#E2E8F0" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#26FFFFFF" />
<corners android:radius="999dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="270"
android:startColor="#2E6BFF"
android:endColor="#1D4ED8" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<corners android:radius="12dp" />
<stroke
android:width="1dp"
android:color="#CBD5E1" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#F1F5F9" />
<corners android:radius="16dp" />
</shape>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,6.4 A4.2,4.2 0 1 1 12,14.8 A4.2,4.2 0 1 1 12,6.4 Z"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.7"
android:strokeLineCap="round"
android:fillColor="#00000000" />
<path
android:pathData="M6.2,20.2 A5.8,5.8 0 0 1 17.8,20.2"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.7"
android:strokeLineCap="round"
android:fillColor="#00000000" />
<path
android:pathData="M4,8.2 L4,5.2 C4,4.55 4.55,4 5.2,4 L8.2,4"
android:strokeColor="#7DD3FC"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M15.8,4 L18.8,4 C19.45,4 20,4.55 20,5.2 L20,8.2"
android:strokeColor="#7DD3FC"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M4,15.8 L4,18.8 C4,19.45 4.55,20 5.2,20 L8.2,20"
android:strokeColor="#7DD3FC"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M15.8,20 L18.8,20 C19.45,20 20,19.45 20,18.8 L20,15.8"
android:strokeColor="#7DD3FC"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>

View File

@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#1D4ED8" /> <gradient
android:angle="315"
android:startColor="#3B82F6"
android:centerColor="#2563EB"
android:endColor="#1E40AF" />
</shape> </shape>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M54,32 A15,15 0 1 1 54,62 A15,15 0 1 1 54,32 Z"
android:strokeColor="#FFFFFF"
android:strokeWidth="5"
android:strokeLineCap="round"
android:fillColor="#00000000" />
<path
android:pathData="M36,84 A18,18 0 0 1 72,84"
android:strokeColor="#FFFFFF"
android:strokeWidth="5"
android:strokeLineCap="round"
android:fillColor="#00000000" />
<path
android:pathData="M25,40 L25,33 C25,30.6 26.6,29 29,29 L36,29"
android:strokeColor="#7DD3FC"
android:strokeWidth="5"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M72,29 L79,29 C81.4,29 83,30.6 83,33 L83,40"
android:strokeColor="#7DD3FC"
android:strokeWidth="5"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M25,68 L25,75 C25,77.4 26.6,79 29,79 L36,79"
android:strokeColor="#7DD3FC"
android:strokeWidth="5"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M72,79 L79,79 C81.4,79 83,77.4 83,75 L83,68"
android:strokeColor="#7DD3FC"
android:strokeWidth="5"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6.2,18.6 L17.8,18.6 A3.2,3.2 0 0 0 17.9,12.3 A4.6,4.6 0 0 0 8.9,10.6 A3.6,3.6 0 0 0 6.2,18.6 Z"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M12,14.6 L12,8.4"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:fillColor="#00000000" />
<path
android:pathData="M9.2,11.2 L12,8.4 L14.8,11.2"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
</vector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_background" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_background" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,7 +2,11 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:fontFamily">sans</item> <item name="android:fontFamily">sans</item>
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">false</item>
<item name="android:statusBarColor">#1D4ED8</item>
<item name="android:windowBackground">#F1F5F9</item>
<item name="android:colorAccent">#2563EB</item> <item name="android:colorAccent">#2563EB</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:navigationBarColor">#F1F5F9</item>
</style> </style>
</resources> </resources>