人脸检测UI
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.example.defenseapplication.okhttp
|
||||||
|
|
||||||
|
data class Post(
|
||||||
|
val userId: Int,
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
val body: String
|
||||||
|
)
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.example.defenseapplication.okhttp
|
|
||||||
|
|
||||||
class OkHttpClient {
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
app/src/main/res/drawable/bg_bottom_gradient.xml
Normal file
6
app/src/main/res/drawable/bg_bottom_gradient.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user