Update Android app login, home, and face verification flows.

Sync current local UI and activity changes to remote repository.

Made-with: Cursor
This commit is contained in:
2026-04-27 15:39:47 +08:00
parent 2e6aba459c
commit b47704b281
24 changed files with 876 additions and 204 deletions

View File

@@ -5,17 +5,75 @@ import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.HomeActivityBinding
import com.example.defenseapplication.databinding.HomeMenuFragmentBinding
class HomeActivity : AppCompatActivity() {
// ViewBinding 核心对象
private lateinit var binding: HomeActivityBinding
// 定义两个 Fragment
private val homeMenuFragment = HomeMenuFragment()
private val personalFragment = PersonalFragment()
// 当前显示的 Fragment
private var currentFragment: Fragment = homeMenuFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.home_activity)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
// 初始化 ViewBinding
binding = HomeActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
// 默认显示首页
switchFragment(homeMenuFragment)
// 第一次加载,只 add 首页
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, homeMenuFragment)
.commit()
// ========== 首页点击 ==========
binding.tabHome.setOnClickListener {
switchFragment(homeMenuFragment)
// 切换图标 + 文字颜色
binding.ivHome.setImageResource(R.drawable.home_checked)
binding.tvHome.setTextColor(resources.getColor(R.color.ic_selected))
binding.ivProfile.setImageResource(R.drawable.profile)
binding.tvProfile.setTextColor(resources.getColor(R.color.ic_normal))
}
// ========== 个人点击 ==========
binding.tabProfile.setOnClickListener {
switchFragment(personalFragment)
// 切换图标 + 文字颜色
binding.ivHome.setImageResource(R.drawable.home)
binding.tvHome.setTextColor(resources.getColor(R.color.ic_normal))
binding.ivProfile.setImageResource(R.drawable.profile_checked)
binding.tvProfile.setTextColor(resources.getColor(R.color.ic_selected))
}
}
/**
* 切换 Fragment 的方法
*/
private fun switchFragment(fragment: Fragment) {
if (currentFragment == fragment) return
val transaction = supportFragmentManager.beginTransaction()
// 如果没添加过,才 add
if (!fragment.isAdded) {
transaction.add(R.id.fragment_container, fragment)
}
// 隐藏当前,显示目标
transaction.hide(currentFragment)
transaction.show(fragment)
transaction.commit()
currentFragment = fragment
}
}

View File

@@ -6,55 +6,24 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.HomeMenuFragmentBinding
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [HomeMenuFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class HomeMenuFragment : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
// ViewBinding
private var _binding: HomeMenuFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.home_menu_fragment, container, false)
): View {
_binding = HomeMenuFragmentBinding.inflate(inflater, container, false)
return binding.root
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment HomeMenuFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
HomeMenuFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 避免内存泄漏
}
}

View File

@@ -6,55 +6,24 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.PersonalFragmentBinding
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [PersonalFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class PersonalFragment : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
// ViewBinding
private var _binding: PersonalFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.personal_fragment, container, false)
): View {
_binding = PersonalFragmentBinding.inflate(inflater, container, false)
return binding.root
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment PersonalFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
PersonalFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 避免内存泄漏
}
}

View File

@@ -1,21 +1,39 @@
package com.example.defenseapplication.login
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.FaceLoginActivityBinding
class FaceLoginActivity : AppCompatActivity() {
private lateinit var binding: FaceLoginActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.face_login_activity)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
binding = FaceLoginActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
setupListeners()
}
private fun setupListeners() {
// 返回按钮
binding.backIcon.setOnClickListener {
finish()
}
// 继续按钮
binding.btnStartVerification.setOnClickListener {
val intent= Intent(this, FaceVerificationActivity::class.java)
startActivity(intent)
Toast.makeText(this, "跳转到人脸识别页面", Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -1,21 +1,110 @@
package com.example.defenseapplication.login
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.defenseapplication.R
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.defenseapplication.databinding.FaceVerificationActivityBinding
import com.example.defenseapplication.view.FaceAnalyzer
import com.google.mlkit.vision.face.FaceDetection
import com.google.mlkit.vision.face.FaceDetectorOptions
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class FaceVerificationActivity : AppCompatActivity() {
private lateinit var binding: FaceVerificationActivityBinding
private lateinit var cameraExecutor: ExecutorService
private var faceDetector: com.google.mlkit.vision.face.FaceDetector? = null
companion object {
private const val REQUEST_CAMERA_PERMISSION = 100
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.face_verification_activity)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
// 使用标准 ViewBinding
binding = FaceVerificationActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
// 初始化人脸检测器(选项可自行调整)
val options = FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
.build()
faceDetector = FaceDetection.getClient(options)
cameraExecutor = Executors.newSingleThreadExecutor()
// 检查并请求权限
checkCameraPermissionAndStart()
}
private fun checkCameraPermissionAndStart() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
REQUEST_CAMERA_PERMISSION
)
}
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 预览用例
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
// 分析用例(人脸检测)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(cameraExecutor, FaceAnalyzer(faceDetector, binding.overlay))
// 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// 绑定到生命周期
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis)
}, ContextCompat.getMainExecutor(this))
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CAMERA_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera()
} else {
Toast.makeText(this, "需要相机权限才能进行人脸验证", Toast.LENGTH_SHORT).show()
finish()
}
}
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
faceDetector?.close()
}
}

View File

@@ -12,7 +12,6 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.ForgetPasswordActivityBinding
import com.example.defenseapplication.okhttp.OkHttpClientManager
import java.util.regex.Pattern
import kotlin.random.Random
@@ -79,20 +78,6 @@ class ForgetPasswordActivity : AppCompatActivity() {
isSendingCode = true
binding.sendCodeBtn.isEnabled = false
OkHttpClientManager.postForm(
url = "https://httpbin.org/post",
params = mapOf("phone" to phoneNumber)
) { result ->
result.onSuccess {
verifyCode = (100000 + Random.nextInt(900000)).toString()
startCountDown()
Toast.makeText(this, "验证码已发送(演示码:$verifyCode", Toast.LENGTH_SHORT).show()
}.onFailure {
Toast.makeText(this@ForgetPasswordActivity, "发送失败,请重试", Toast.LENGTH_SHORT).show()
resetSendButton()
}
isSendingCode = false
}
}
/**
@@ -181,20 +166,7 @@ class ForgetPasswordActivity : AppCompatActivity() {
}
""".trimIndent()
OkHttpClientManager.postJson(
url = "https://httpbin.org/post",
json = requestJson
) { result ->
result.onSuccess {
Toast.makeText(this, "密码重置成功,请重新登录", Toast.LENGTH_LONG).show()
finish()
}.onFailure {
Toast.makeText(this, "网络异常,请重试", Toast.LENGTH_SHORT).show()
}
binding.btnNext.isEnabled = true
binding.btnNext.text = "确定"
}
}
override fun onDestroy() {

View File

@@ -1,6 +1,8 @@
package com.example.defenseapplication.login
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.defenseapplication.databinding.RegisterActivityBinding
@@ -31,6 +33,9 @@ class RegisterActivity : AppCompatActivity() {
// 下一步按钮
binding.btnNext.setOnClickListener {
if (!isLoading) {
val intent= Intent(this, FaceLoginActivity::class.java)
startActivity(intent)
Toast.makeText(this, "跳转到人脸页面", Toast.LENGTH_SHORT).show()
//performRegistration()
}
}

View File

@@ -1,21 +1,63 @@
package com.example.defenseapplication.login
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.RegisterSuccessActivityBinding
import com.example.defenseapplication.home.HomeActivity
class RegisterSuccessActivity : AppCompatActivity() {
private lateinit var binding: RegisterSuccessActivityBinding
private var countDownTimer: CountDownTimer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.register_success_activity)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
binding = RegisterSuccessActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
// 返回按钮
binding.backIcon.setOnClickListener {
finish()
}
// 点击按钮:取消倒计时并立即跳转
binding.btnEnter.setOnClickListener {
cancelTimer()
jumpToPlatform()
}
// 启动3秒倒计时自动跳转
startAutoJumpTimer()
}
private fun startAutoJumpTimer() {
countDownTimer = object : CountDownTimer(3000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
val seconds = (millisUntilFinished / 1000).toInt()
binding.btnEnter.text = "进入平台(${seconds}S)"
}
override fun onFinish() {
binding.btnEnter.text = "进入平台(0S)"
jumpToPlatform()
}
}.start()
}
private fun cancelTimer() {
countDownTimer?.cancel()
countDownTimer = null
}
private fun jumpToPlatform() {
startActivity(Intent(this, HomeActivity::class.java))
finish()
}
override fun onDestroy() {
super.onDestroy()
cancelTimer()
}
}

View File

@@ -1,4 +1,44 @@
package com.example.defenseapplication.view
class FaceAnalyzer {
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.FaceDetector
/*
* 人脸分析器
* */
@ExperimentalGetImage
class FaceAnalyzer(
private val detector: FaceDetector?,
private val overlayView: FaceOverlayView
) : ImageAnalysis.Analyzer {
override fun analyze(imageProxy: ImageProxy) {
if (detector == null) {
imageProxy.close()
return
}
val mediaImage = imageProxy.image ?: run {
imageProxy.close()
return
}
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
detector.process(image)
.addOnSuccessListener { faces ->
Handler(Looper.getMainLooper()).post {
overlayView.setFaces(faces)
}
}
.addOnFailureListener { e ->
Log.e("FaceAnalyzer", "人脸检测失败", e)
}
.addOnCompleteListener {
imageProxy.close()
}
}
}

View File

@@ -1,2 +1,36 @@
package com.example.defenseapplication.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import com.google.mlkit.vision.face.Face
class FaceOverlayView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint().apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 4f
}
private val textPaint = Paint().apply {
color = Color.YELLOW
textSize = 40f
}
private var faces = mutableListOf<Face>()
fun setFaces(faces: List<Face>) {
this.faces = faces.toMutableList()
invalidate() // 触发 onDraw
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
for (face in faces) {
val rect = face.boundingBox
canvas.drawRect(rect, paint)
canvas.drawText("Face", rect.left.toFloat(), rect.top - 10f, textPaint)
}
}
}