人脸检测UI

This commit is contained in:
2026-04-27 17:03:56 +08:00
parent b47704b281
commit 1b62e4d264
9 changed files with 192 additions and 14 deletions

View File

@@ -48,7 +48,17 @@ dependencies {
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
// RecyclerView
implementation("androidx.recyclerview:recyclerview:1.3.2")
// OkHttp
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Gson (用于解析 JSON)
implementation("com.google.code.gson:gson:2.10.1")
// Kotlin 协程
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// CameraX 用于相机预览 // CameraX 用于相机预览
implementation("androidx.camera:camera-camera2:1.3.0") implementation("androidx.camera:camera-camera2:1.3.0")

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-feature <uses-feature

View File

@@ -0,0 +1,16 @@
package com.example.defenseapplication.okhttp
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
object ApiService {
private const val BASE_URL = "https://stem-boozy-margarine.ngrok-free.dev"
private val client = OkHttpClient()
private val gson = Gson()
}

View File

@@ -0,0 +1,8 @@
package com.example.defenseapplication.okhttp
data class Post(
val userId: Int,
val id: Int,
val title: String,
val body: String
)

View File

@@ -1,4 +0,0 @@
package com.example.defenseapplication.okhttp
class OkHttpClient {
}

View File

@@ -4,20 +4,31 @@ import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import com.google.mlkit.vision.face.Face import com.google.mlkit.vision.face.Face
class FaceOverlayView(context: Context, attrs: AttributeSet?) : View(context, attrs) { class FaceOverlayView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint().apply { private val framePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.GREEN color = Color.WHITE
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 4f strokeWidth = 6f
} }
private val textPaint = Paint().apply { private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.YELLOW color = Color.parseColor("#7A000000")
textSize = 40f style = Paint.Style.FILL
} }
private val facePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#80FFFFFF")
style = Paint.Style.STROKE
strokeWidth = 3f
}
private val maskPath = Path()
private val frameRect = RectF()
private var faces = mutableListOf<Face>() private var faces = mutableListOf<Face>()
fun setFaces(faces: List<Face>) { fun setFaces(faces: List<Face>) {
@@ -27,10 +38,27 @@ class FaceOverlayView(context: Context, attrs: AttributeSet?) : View(context, at
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
val cx = width / 2f
val cy = height * 0.45f
val radius = minOf(width, height) * 0.31f
maskPath.reset()
maskPath.addRect(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW)
maskPath.addCircle(cx, cy, radius, Path.Direction.CCW)
canvas.drawPath(maskPath, maskPaint)
canvas.drawCircle(cx, cy, radius, framePaint)
frameRect.set(cx - radius, cy - radius, cx + radius, cy + radius)
for (face in faces) { for (face in faces) {
val rect = face.boundingBox val rect = face.boundingBox
canvas.drawRect(rect, paint) val clippedLeft = rect.left.coerceAtLeast(frameRect.left.toInt()).toFloat()
canvas.drawText("Face", rect.left.toFloat(), rect.top - 10f, textPaint) val clippedTop = rect.top.coerceAtLeast(frameRect.top.toInt()).toFloat()
val clippedRight = rect.right.coerceAtMost(frameRect.right.toInt()).toFloat()
val clippedBottom = rect.bottom.coerceAtMost(frameRect.bottom.toInt()).toFloat()
if (clippedRight > clippedLeft && clippedBottom > clippedTop) {
canvas.drawRect(clippedLeft, clippedTop, clippedRight, clippedBottom, facePaint)
}
} }
} }
} }

View File

@@ -0,0 +1,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#00000000"
android:endColor="#E0000000"
android:angle="90" />
</shape>

View File

@@ -6,7 +6,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".login.FaceVerificationActivity"> tools:context=".login.FaceVerificationActivity">
<androidx.camera.view.PreviewView <androidx.camera.view.PreviewView
android:id="@+id/previewView" android:id="@+id/previewView"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -17,4 +16,118 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<View
android:id="@+id/statusBarPlaceholder"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="#26000000"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/topContainer" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/statusBarPlaceholder"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/backIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_back_arrow"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="人脸采集"
android:textColor="@android:color/white"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/backIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/backIcon" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/hintContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintBottom_toTopOf="@id/bottomContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_red_dot" />
<TextView
android:id="@+id/tvFaceHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请将人脸放入圆形区域内"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold"
android:letterSpacing="0.03" />
</LinearLayout>
<LinearLayout
android:id="@+id/bottomContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingTop="20dp"
android:paddingBottom="28dp"
android:background="@drawable/bg_bottom_gradient"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<android.widget.Button
android:id="@+id/btnCaptureFace"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="采集人脸"
android:textAllCaps="false"
android:textSize="17sp"
android:textStyle="bold"
android:background="@drawable/bg_capture_face_button"
android:textColor="#F2FFFFFF" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="48dp"
android:layout_height="48dp"
android:visibility="gone"
android:background="#AA000000"
android:indeterminateTint="@color/white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -10,5 +10,6 @@
<color name="face">#F0F0F0</color> <color name="face">#F0F0F0</color>
<color name="ic_selected">#000000 </color> <color name="ic_selected">#000000 </color>
<color name="ic_normal">#999999 </color> <color name="ic_normal">#999999 </color>
<color name="ic_gray">#99000000 </color>
</resources> </resources>