Browse Source

人脸登录优化

yaorongkeji 8 months ago
parent
commit
25af416f94

+ 2 - 2
baselib/build.gradle

@@ -38,8 +38,8 @@ android {
             buildConfigField "Boolean", "SINGLE_MODULE", "${singleModule}"
 //            buildConfigField "String", "BUGLY_APPID", "\"d400f20398\""
 //            buildConfigField "String", "API_URL", "\"http://60.205.122.15/hcp-app-api\""
-//            buildConfigField "String", "API_URL", "\"http://192.168.0.118:8110\""
-            buildConfigField "String", "API_URL", "\"http://yaorongtest.yaorongmedical.com/hcp-app-api\""
+            buildConfigField "String", "API_URL", "\"http://192.168.0.118:8110\""
+//            buildConfigField "String", "API_URL", "\"http://yaorongtest.yaorongmedical.com/hcp-app-api\""
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
         }
 

+ 5 - 0
baselib/src/main/java/com/yingyangfly/baselib/router/RouterUrlCommon.kt

@@ -264,4 +264,9 @@ object RouterUrlCommon {
      * 人脸登录
      */
     const val humanFaceLogin = "/login/humanFaceLogin"
+
+    /**
+     * 图片上传
+     */
+    const val faceRegister =  "/personalCenter/faceRegister"
 }

+ 6 - 6
home/src/main/java/com/yingyangfly/home/activity/HomeActivity.kt

@@ -571,12 +571,12 @@ class HomeActivity : BaseMVVMActivity<ActivityHomeBinding, HomeViewModel>(), Vie
 
                     R.id.tvHealthCounseling -> {
                         //健康咨询
-//                        JumpUtil.jumpActivity(RouterUrlCommon.healthConsultation, mContext)
-                        val builder = AlertDialog.Builder(this)
-                        builder.setTitle("提示")
-                        builder.setMessage("暂未开通此功能")
-                        builder.setPositiveButton("确定") { dialog, _ -> dialog.dismiss() }
-                        builder.create().show()
+                        JumpUtil.jumpActivity(RouterUrlCommon.healthConsultation, mContext)
+//                        val builder = AlertDialog.Builder(this)
+//                        builder.setTitle("提示")
+//                        builder.setMessage("暂未开通此功能")
+//                        builder.setPositiveButton("确定") { dialog, _ -> dialog.dismiss() }
+//                        builder.create().show()
                     }
 
 //                    R.id.tvfunTime -> {

+ 1 - 0
login/build.gradle

@@ -22,6 +22,7 @@ dependencies {
     implementation(rootProject.ext.androidx.material)
     implementation(rootProject.ext.androidx.constraintlayout)
 
+
     implementation "androidx.camera:camera-core:1.0.2"
     implementation "androidx.camera:camera-camera2:1.0.2"
     implementation "androidx.camera:camera-lifecycle:1.0.2"

+ 184 - 6
login/src/main/java/com/yingyang/login/ui/login/FaceCaptureActivity.kt

@@ -3,9 +3,13 @@ package com.yingyang.login.ui.login
 import android.Manifest
 import android.content.Intent
 import android.content.pm.PackageManager
+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.ExifInterface
 import android.os.Build
 import android.os.Bundle
 import android.os.Handler
@@ -36,6 +40,8 @@ import com.google.mlkit.vision.face.FaceDetection
 import com.google.mlkit.vision.face.FaceDetectorOptions
 import com.yingyang.login.service.FaceOverlayView
 import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.FileOutputStream
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
@@ -97,6 +103,12 @@ class FaceCaptureActivity : AppCompatActivity() {
     private var lastCaptureTime = 0L
     private val minCaptureInterval = 2000L // 最小2秒间隔
 
+    // 新增:人脸裁剪和压缩参数
+    private val COMPRESSED_IMAGE_MAX_SIZE = 800 // 压缩后的最大边长(像素)
+    private val COMPRESSED_IMAGE_QUALITY = 80 // JPEG压缩质量(0-100)
+    private val FACE_CROP_MARGIN = 0.2f // 人脸裁剪边距(相对于人脸框的比例)
+    private val MIN_FACE_SIZE_FOR_CROP = 100 // 最小人脸尺寸(像素),用于判断是否进行裁剪
+
     companion object {
         private const val TAG = "FaceCapture"
         private const val REQUEST_CODE_PERMISSIONS = 100
@@ -343,7 +355,7 @@ class FaceCaptureActivity : AppCompatActivity() {
                     if (stableFaceFrames >= requiredStableFrames) {
                         // 人脸稳定,触发自动拍照
                         lastDetectionTime = System.currentTimeMillis()
-                        autoCaptureFace(imageProxy)
+                        autoCaptureFace(imageProxy, face)
                         return@runOnUiThread
                     }
                 } else {
@@ -384,7 +396,7 @@ class FaceCaptureActivity : AppCompatActivity() {
         updateStatus("已停止")
     }
 
-    private fun autoCaptureFace(imageProxy: ImageProxy) {
+    private fun autoCaptureFace(imageProxy: ImageProxy, face: Face) {
         // 新增:添加双重检查,确保不会同时处理多个请求
         if (isProcessingPhoto || isRequestInProgress) {
             Log.d(TAG, "已有请求在处理中,跳过本次拍照")
@@ -402,8 +414,8 @@ class FaceCaptureActivity : AppCompatActivity() {
         lastDetectionTime = System.currentTimeMillis()
         lastCaptureTime = System.currentTimeMillis()
 
-        // 从 ImageProxy 获取图片并转换为 Base64
-        val base64Image = convertImageProxyToBase64(imageProxy)
+        // 从 ImageProxy 获取图片并转换为 Base64(带压缩)
+        val base64Image = convertAndCompressFaceImage(imageProxy, face)
         imageProxy.close()
 
         if (base64Image.isNotEmpty()) {
@@ -416,11 +428,177 @@ class FaceCaptureActivity : AppCompatActivity() {
         }
     }
 
+    /**
+     * 新增:转换并压缩人脸图片
+     * 步骤:
+     * 1. 将ImageProxy转换为Bitmap
+     * 2. 根据人脸位置裁剪人脸区域
+     * 3. 压缩裁剪后的人脸图片
+     * 4. 转换为Base64
+     */
+    private fun convertAndCompressFaceImage(imageProxy: ImageProxy, face: Face): String {
+        val mediaImage = imageProxy.image ?: return ""
+        val rotationDegrees = imageProxy.imageInfo.rotationDegrees
+
+        try {
+            // 步骤1:将YUV_420_888转换为Bitmap
+            val bitmap = convertYuvToBitmap(mediaImage, rotationDegrees)
+                ?: return convertImageProxyToBase64(imageProxy) // 如果转换失败,回退到原始方法
+
+            // 步骤2:根据人脸位置裁剪人脸区域
+            val croppedBitmap = cropFaceFromBitmap(bitmap, face, imageProxy.width, imageProxy.height)
+                ?: return compressBitmapToBase64(bitmap) // 如果裁剪失败,压缩整个图片
+
+            // 步骤3:压缩裁剪后的人脸图片
+            val compressedBitmap = compressBitmap(croppedBitmap)
+
+            // 步骤4:转换为Base64
+            return bitmapToBase64(compressedBitmap)
+
+        } catch (e: Exception) {
+            Log.e(TAG, "人脸图片压缩失败: ${e.message}")
+            // 如果压缩失败,回退到原始方法
+            return convertImageProxyToBase64(imageProxy)
+        }
+    }
+
+    /**
+     * 将YUV_420_888图像转换为Bitmap
+     */
+    private fun convertYuvToBitmap(mediaImage: android.media.Image, rotationDegrees: Int): Bitmap? {
+        try {
+            val yBuffer = mediaImage.planes[0].buffer
+            val uBuffer = mediaImage.planes[1].buffer
+            val vBuffer = mediaImage.planes[2].buffer
+
+            val ySize = yBuffer.remaining()
+            val uSize = uBuffer.remaining()
+            val vSize = vBuffer.remaining()
+
+            val nv21 = ByteArray(ySize + uSize + vSize)
+
+            // Y plane
+            yBuffer.get(nv21, 0, ySize)
+            // V plane
+            vBuffer.get(nv21, ySize, vSize)
+            // U plane
+            uBuffer.get(nv21, ySize + vSize, uSize)
+
+            val yuvImage = YuvImage(nv21, ImageFormat.NV21, mediaImage.width, mediaImage.height, null)
+            val outputStream = ByteArrayOutputStream()
+            yuvImage.compressToJpeg(Rect(0, 0, mediaImage.width, mediaImage.height), 90, outputStream)
+
+            val jpegBytes = outputStream.toByteArray()
+            var bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
+
+            // 根据旋转角度调整图片
+            if (rotationDegrees != 0 && bitmap != null) {
+                val matrix = Matrix()
+                matrix.postRotate(rotationDegrees.toFloat())
+                bitmap = Bitmap.createBitmap(
+                    bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
+                )
+            }
+
+            return bitmap
+        } catch (e: Exception) {
+            Log.e(TAG, "YUV转Bitmap失败: ${e.message}")
+            return null
+        }
+    }
+
+    /**
+     * 从Bitmap中裁剪人脸区域
+     */
+    private fun cropFaceFromBitmap(bitmap: Bitmap, face: Face, imageWidth: Int, imageHeight: Int): Bitmap? {
+        val faceRect = face.boundingBox
+
+        // 如果人脸太小,不进行裁剪
+        if (faceRect.width() < MIN_FACE_SIZE_FOR_CROP || faceRect.height() < MIN_FACE_SIZE_FOR_CROP) {
+            return null
+        }
+
+        // 计算裁剪区域(添加边距)
+        val marginX = (faceRect.width() * FACE_CROP_MARGIN).toInt()
+        val marginY = (faceRect.height() * FACE_CROP_MARGIN).toInt()
+
+        var left = faceRect.left - marginX
+        var top = faceRect.top - marginY
+        var right = faceRect.right + marginX
+        var bottom = faceRect.bottom + marginY
+
+        // 确保裁剪区域在图片范围内
+        left = left.coerceAtLeast(0)
+        top = top.coerceAtLeast(0)
+        right = right.coerceAtMost(bitmap.width)
+        bottom = bottom.coerceAtMost(bitmap.height)
+
+        // 确保裁剪区域有效
+        if (left >= right || top >= bottom || right - left < 50 || bottom - top < 50) {
+            return null
+        }
+
+        return try {
+            Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top)
+        } catch (e: Exception) {
+            Log.e(TAG, "人脸裁剪失败: ${e.message}")
+            null
+        }
+    }
+
+    /**
+     * 压缩Bitmap
+     */
+    private fun compressBitmap(bitmap: Bitmap): Bitmap {
+        var width = bitmap.width
+        var height = bitmap.height
+
+        // 计算压缩比例
+        if (width > COMPRESSED_IMAGE_MAX_SIZE || height > COMPRESSED_IMAGE_MAX_SIZE) {
+            val scale = if (width > height) {
+                COMPRESSED_IMAGE_MAX_SIZE.toFloat() / width
+            } else {
+                COMPRESSED_IMAGE_MAX_SIZE.toFloat() / height
+            }
+
+            width = (width * scale).toInt()
+            height = (height * scale).toInt()
+        }
+
+        // 如果尺寸已经小于等于目标尺寸,直接返回
+        if (width == bitmap.width && height == bitmap.height) {
+            return bitmap
+        }
+
+        // 创建压缩后的Bitmap
+        return Bitmap.createScaledBitmap(bitmap, width, height, true)
+    }
+
+    /**
+     * 将Bitmap转换为Base64
+     */
+    private fun bitmapToBase64(bitmap: Bitmap): String {
+        val outputStream = ByteArrayOutputStream()
+        bitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESSED_IMAGE_QUALITY, outputStream)
+        val imageBytes = outputStream.toByteArray()
+        return Base64.encodeToString(imageBytes, Base64.NO_WRAP)
+    }
+
+    /**
+     * 压缩整个Bitmap并转换为Base64(裁剪失败时的备选方案)
+     */
+    private fun compressBitmapToBase64(bitmap: Bitmap): String {
+        val compressedBitmap = compressBitmap(bitmap)
+        return bitmapToBase64(compressedBitmap)
+    }
+
+    /**
+     * 原始方法:将ImageProxy转换为Base64(保留作为备份)
+     */
     private fun convertImageProxyToBase64(imageProxy: ImageProxy): String {
         val mediaImage = imageProxy.image ?: return ""
 
         try {
-            // 将 YUV_420_888 转换为 JPEG
             val yBuffer = mediaImage.planes[0].buffer
             val uBuffer = mediaImage.planes[1].buffer
             val vBuffer = mediaImage.planes[2].buffer
@@ -465,7 +643,7 @@ class FaceCaptureActivity : AppCompatActivity() {
         }
 
         viewModel.humanFaceLogin(
-              base64Image,
+            base64Image,
             fail = { errorMessage ->
                 runOnUiThread {
                     handleServerResponse(

+ 12 - 0
personalcenter/build.gradle

@@ -21,4 +21,16 @@ dependencies {
     implementation(rootProject.ext.androidx.appcompat)
     implementation(rootProject.ext.androidx.material)
     implementation(rootProject.ext.androidx.constraintlayout)
+
+    implementation "androidx.camera:camera-core:1.0.2"
+    implementation "androidx.camera:camera-camera2:1.0.2"
+    implementation "androidx.camera:camera-lifecycle:1.0.2"
+    implementation "androidx.camera:camera-view:1.0.0-alpha27"
+    // 注意:camera-extensions 在某些设备上可能不稳定,如无特效需求可先注释
+    implementation "androidx.camera:camera-extensions:1.0.0-alpha27"
+
+
+//    // 其他库
+    implementation "com.guolindev.permissionx:permissionx:1.6.4"
+    implementation "com.google.mlkit:face-detection:16.1.5"
 }

+ 12 - 1
personalcenter/src/main/AndroidManifest.xml

@@ -2,9 +2,14 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.yingyang.personalcenter">
 
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="28" /> <!-- Android 9及以下 -->
+
     <application
         android:allowBackup="true"
-        android:supportsRtl="true">
+        android:supportsRtl="true"
+        android:requestLegacyExternalStorage="true">
         <activity
             android:name=".setting.SettingActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
@@ -50,6 +55,12 @@
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:screenOrientation="landscape"
             android:windowSoftInputMode="adjustResize|adjustPan" />
+
+        <activity
+            android:name=".personalcenter.FaceRegisterActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="landscape"
+            android:windowSoftInputMode="adjustResize|adjustPan" />
     </application>
 
 </manifest>

+ 36 - 0
personalcenter/src/main/java/com/yingyang/personalcenter/net/PersonalCenterApiService.kt

@@ -4,7 +4,13 @@ import com.yingyang.personalcenter.entity.VersionBean
 import com.yingyangfly.baselib.bean.OrgBean
 import com.yingyangfly.baselib.bean.UserInfoBean
 import com.yingyangfly.baselib.net.BaseResp
+import okhttp3.MultipartBody
+import retrofit2.http.Body
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
+import retrofit2.http.Multipart
 import retrofit2.http.POST
+import retrofit2.http.Part
 import retrofit2.http.Query
 
 interface PersonalCenterApiService {
@@ -32,4 +38,34 @@ interface PersonalCenterApiService {
      */
     @POST("app/sysOrg/findByOrgCode")
     suspend fun findByOrgCode(@Query("orgCode") orgCode: String): BaseResp<OrgBean>
+
+    /**
+     * 上传文件接口
+     */
+    @Multipart
+    @POST("common/upload")
+    suspend fun uploadFile(
+        @Query("dirName") dirName: String,
+        @Part file: MultipartBody.Part
+    ): BaseResp<String>
+
+    /**
+     * 人脸照片注册
+     */
+    @FormUrlEncoded
+    @POST("app/face/registration")
+    suspend fun faceRegistration(@Field("humanFaceUrl") imageBase: String): BaseResp<String>
+
+    /**
+     * 获取当前账户人脸照片
+     */
+    @POST("face/getHumanFace")
+    suspend fun getHumanFace(): BaseResp<String>
+
+    /**
+     * 删除当前人脸
+     */
+    @POST("face/deleteHumanFace")
+    suspend fun deleteHumanFace(@Query("faceUrl")faceUrl :String): BaseResp<Boolean>
+
 }

+ 613 - 0
personalcenter/src/main/java/com/yingyang/personalcenter/personalcenter/FaceRegisterActivity.kt

@@ -0,0 +1,613 @@
+package com.yingyang.personalcenter.personalcenter
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.graphics.BitmapFactory
+import android.graphics.Rect
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.ProgressBar
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.*
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.ViewModelProvider
+import com.alibaba.android.arouter.facade.annotation.Route
+import com.google.mlkit.vision.common.InputImage
+import com.google.mlkit.vision.face.FaceDetection
+import com.google.mlkit.vision.face.FaceDetectorOptions
+import com.yingyang.personalcenter.R
+import com.yingyangfly.baselib.router.RouterUrlCommon
+import com.yingyangfly.baselib.utils.JumpUtil
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicInteger
+
+@Route(path = RouterUrlCommon.faceRegister)
+class FaceRegisterActivity : AppCompatActivity() {
+
+    // UI组件
+    private lateinit var previewView: PreviewView
+    private lateinit var captureButton: Button
+    private lateinit var statusTextView: TextView
+    private lateinit var progressBar: ProgressBar
+    private lateinit var iconBack: ImageView
+    private lateinit var faceGuideView: View // 人脸引导框
+
+    // CameraX
+    private lateinit var cameraProviderFuture: com.google.common.util.concurrent.ListenableFuture<ProcessCameraProvider>
+    private var imageCapture: ImageCapture? = null
+    private var imageAnalyzer: ImageAnalysis? = null
+    private var camera: Camera? = null
+    private lateinit var cameraExecutor: ExecutorService
+
+    // 人脸检测
+    private lateinit var faceDetector: com.google.mlkit.vision.face.FaceDetector
+
+    // 状态控制
+    private var isProcessingPhoto = false
+    private var isRequestInProgress = false
+    private val captureAttempts = AtomicInteger(0)
+    private val maxCaptureAttempts = 3
+    private var currentAttempt = 1
+
+    // 人脸质量检测(保留用于引导用户)
+    private var stableFaceFrames = 0
+    private val requiredStableFrames = 5
+    private var lastFaceSize = 0f
+    private var lastFacePositionX = 0f
+    private var lastFacePositionY = 0f
+    private var isFaceDetected = false
+    private var isFaceQualityGood = false
+
+    // Handler
+    private val mainHandler = Handler(Looper.getMainLooper())
+
+    // ViewModel
+    private lateinit var viewModel: PersonalCenterViewModel
+
+    // 权限请求
+    private val requestPermissionLauncher = registerForActivityResult(
+        ActivityResultContracts.RequestPermission()
+    ) { isGranted ->
+        if (isGranted) {
+            startCamera()
+        } else {
+            Toast.makeText(this, "需要相机权限才能使用人脸注册", Toast.LENGTH_LONG).show()
+            finish()
+        }
+    }
+
+    companion object {
+        private const val TAG = "FaceRegister"
+        private const val FACE_SIZE_THRESHOLD = 0.15f
+        private const val FACE_MOVEMENT_THRESHOLD = 0.05f
+        private const val FACE_CENTER_THRESHOLD = 0.15f
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_face_register_simple)
+
+        initViews()
+        initViewModel()
+        initFaceDetector()
+
+        if (allPermissionsGranted()) {
+            startCamera()
+        } else {
+            requestCameraPermission()
+        }
+    }
+
+    private fun initViews() {
+        previewView = findViewById(R.id.preview_view)
+        captureButton = findViewById(R.id.capture_button)
+        statusTextView = findViewById(R.id.status_text)
+        progressBar = findViewById(R.id.progress_bar)
+        iconBack = findViewById(R.id.icon_back)
+        faceGuideView = findViewById(R.id.overlay_view) // 人脸引导框
+
+        cameraExecutor = Executors.newSingleThreadExecutor()
+
+        captureButton.setOnClickListener {
+            if (!isProcessingPhoto && !isRequestInProgress) {
+                if (isFaceQualityGood) {
+                    manualCaptureFace()
+                } else {
+                    Toast.makeText(this, "请先调整人脸位置,确保在引导框内", Toast.LENGTH_SHORT).show()
+                }
+            } else {
+                Toast.makeText(this, "正在处理中,请稍候", Toast.LENGTH_SHORT).show()
+            }
+        }
+
+        iconBack.setOnClickListener {
+            JumpUtil.jumpActivity(RouterUrlCommon.personalCenter, this)
+            finish()
+        }
+
+        updateUIState()
+    }
+
+    private fun initViewModel() {
+        viewModel = ViewModelProvider(this)[PersonalCenterViewModel::class.java]
+    }
+
+    private fun initFaceDetector() {
+        val options = FaceDetectorOptions.Builder()
+            .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
+            .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
+            .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE)
+            .setMinFaceSize(0.1f)
+            .build()
+
+        faceDetector = FaceDetection.getClient(options)
+    }
+
+    private fun allPermissionsGranted(): Boolean {
+        return ContextCompat.checkSelfPermission(
+            this, Manifest.permission.CAMERA
+        ) == PackageManager.PERMISSION_GRANTED
+    }
+
+    private fun requestCameraPermission() {
+        requestPermissionLauncher.launch(Manifest.permission.CAMERA)
+    }
+
+    private fun startCamera() {
+        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+
+        cameraProviderFuture.addListener(Runnable {
+            val cameraProvider = cameraProviderFuture.get()
+            val rotation = previewView.display.rotation
+
+            // 预览配置
+            val preview = Preview.Builder()
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setTargetRotation(rotation)
+                .build()
+                .also {
+                    it.setSurfaceProvider(previewView.surfaceProvider)
+                }
+
+            // 图像捕获配置
+            imageCapture = ImageCapture.Builder()
+                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setTargetRotation(rotation)
+                .setFlashMode(ImageCapture.FLASH_MODE_OFF)
+                .build()
+
+            // 图像分析配置(用于人脸质量检测)
+            imageAnalyzer = ImageAnalysis.Builder()
+                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+                .setTargetRotation(rotation)
+                .apply {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                        setTargetResolution(android.util.Size(640, 480))
+                    }
+                }
+                .build()
+                .also { analyzer ->
+                    analyzer.setAnalyzer(cameraExecutor, ImageAnalysis.Analyzer { imageProxy ->
+                        analyzeFaceQuality(imageProxy)
+                    })
+                }
+
+            // 选择前置摄像头
+            val cameraSelector = CameraSelector.Builder()
+                .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+                .build()
+
+            try {
+                cameraProvider.unbindAll()
+                camera = cameraProvider.bindToLifecycle(
+                    this, cameraSelector, preview, imageCapture, imageAnalyzer
+                )
+                updateStatus("摄像头就绪,请将人脸对准引导框")
+            } catch (e: Exception) {
+                Log.e(TAG, "相机启动失败: ${e.message}")
+                updateStatus("相机启动失败")
+                Toast.makeText(this, "相机启动失败: ${e.message}", Toast.LENGTH_LONG).show()
+            }
+        }, ContextCompat.getMainExecutor(this))
+    }
+
+    /**
+     * 人脸质量分析(保留校验功能,但不自动拍摄)
+     */
+    private fun analyzeFaceQuality(imageProxy: ImageProxy) {
+        if (isProcessingPhoto || isRequestInProgress) {
+            imageProxy.close()
+            return
+        }
+
+        val mediaImage = imageProxy.image
+        if (mediaImage == null) {
+            imageProxy.close()
+            return
+        }
+
+        val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
+
+        faceDetector.process(image)
+            .addOnSuccessListener { faces ->
+                handleFaceDetectionResult(faces, imageProxy)
+            }
+            .addOnFailureListener { e ->
+                Log.e(TAG, "人脸检测失败: ${e.message}")
+                imageProxy.close()
+            }
+            .addOnCompleteListener {
+                imageProxy.close()
+            }
+    }
+
+    /**
+     * 处理人脸检测结果(仅用于质量提示,不自动拍摄)
+     */
+    private fun handleFaceDetectionResult(faces: List<com.google.mlkit.vision.face.Face>, imageProxy: ImageProxy) {
+        runOnUiThread {
+            if (faces.isEmpty()) {
+                // 未检测到人脸
+                stableFaceFrames = 0
+                isFaceDetected = false
+                isFaceQualityGood = false
+                updateStatus("未检测到人脸,请正对摄像头")
+                updateCaptureButtonState(false)
+                return@runOnUiThread
+            }
+
+            val face = faces.first()
+            val boundingBox = face.boundingBox
+
+            // 计算人脸位置和大小
+            val faceCenterX = (boundingBox.left + boundingBox.right) / 2f / imageProxy.width
+            val faceCenterY = (boundingBox.top + boundingBox.bottom) / 2f / imageProxy.height
+            val faceSize = boundingBox.width().toFloat() / imageProxy.width
+
+            // 检查人脸质量
+            val isFaceCentered = Math.abs(faceCenterX - 0.5f) < FACE_CENTER_THRESHOLD &&
+                    Math.abs(faceCenterY - 0.5f) < FACE_CENTER_THRESHOLD
+            val isFaceGoodSize = faceSize > FACE_SIZE_THRESHOLD && faceSize < 0.6f
+            val isSingleFace = faces.size == 1
+
+            isFaceDetected = true
+
+            if (isFaceCentered && isFaceGoodSize && isSingleFace) {
+                // 检查人脸稳定性
+                if (stableFaceFrames == 0 ||
+                    (Math.abs(faceSize - lastFaceSize) < FACE_MOVEMENT_THRESHOLD &&
+                            Math.abs(faceCenterX - lastFacePositionX) < FACE_MOVEMENT_THRESHOLD &&
+                            Math.abs(faceCenterY - lastFacePositionY) < FACE_MOVEMENT_THRESHOLD)) {
+
+                    stableFaceFrames++
+
+                    if (stableFaceFrames >= requiredStableFrames) {
+                        // 人脸质量良好,可以拍摄
+                        isFaceQualityGood = true
+                        updateStatus("人脸质量良好,可以点击拍照")
+                        updateCaptureButtonState(true)
+                    } else {
+                        // 人脸稳定中
+                        isFaceQualityGood = false
+                        updateStatus("请保持姿势稳定 (${stableFaceFrames}/${requiredStableFrames})")
+                        updateCaptureButtonState(false)
+                    }
+                } else {
+                    // 人脸移动,重置稳定性计数
+                    stableFaceFrames = 1
+                    isFaceQualityGood = false
+                    updateStatus("检测到人脸,请保持稳定")
+                    updateCaptureButtonState(false)
+                }
+
+                lastFaceSize = faceSize
+                lastFacePositionX = faceCenterX
+                lastFacePositionY = faceCenterY
+            } else {
+                // 人脸质量不符合要求
+                stableFaceFrames = 0
+                isFaceQualityGood = false
+                updateCaptureButtonState(false)
+
+                when {
+                    !isSingleFace -> updateStatus("检测到多张人脸,请确保只有一人")
+                    !isFaceCentered -> updateStatus("请将人脸对准引导框中心")
+                    !isFaceGoodSize -> {
+                        if (faceSize <= FACE_SIZE_THRESHOLD) {
+                            updateStatus("请靠近摄像头")
+                        } else {
+                            updateStatus("请远离摄像头")
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 更新拍照按钮状态
+     */
+    private fun updateCaptureButtonState(enabled: Boolean) {
+        runOnUiThread {
+            if (enabled && !isProcessingPhoto && !isRequestInProgress) {
+                captureButton.isEnabled = true
+                captureButton.alpha = 1f
+                captureButton.text = "拍照"
+            } else {
+                captureButton.isEnabled = false
+                captureButton.alpha = 0.6f
+                if (isProcessingPhoto || isRequestInProgress) {
+                    captureButton.text = "处理中..."
+                } else {
+                    captureButton.text = "调整姿势"
+                }
+            }
+        }
+    }
+
+    private fun manualCaptureFace() {
+        if (isProcessingPhoto || isRequestInProgress || !isFaceQualityGood) {
+            return
+        }
+
+        isProcessingPhoto = true
+        isRequestInProgress = true
+        updateStatus("正在拍照...")
+        showProgressBar(true)
+        updateUIState()
+
+        // 使用ImageCapture拍照
+        captureImageToFile()
+    }
+
+    private fun captureImageToFile() {
+        val timestamp = System.currentTimeMillis()
+        val fileName = "face_register_$timestamp.jpg"
+        val file = File(cacheDir, fileName)
+
+        val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()
+
+        imageCapture?.takePicture(
+            outputFileOptions,
+            ContextCompat.getMainExecutor(this),
+            object : ImageCapture.OnImageSavedCallback {
+                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
+                    Log.d(TAG, "图片保存成功: ${file.absolutePath}")
+
+                    runOnUiThread {
+                        if (file.exists() && file.length() > 1024) {
+                            processCapturedImage(file)
+                        } else {
+                            handleCaptureError("保存的图片文件无效")
+                            if (file.exists()) file.delete()
+                        }
+                    }
+                }
+
+                override fun onError(exception: ImageCaptureException) {
+                    Log.e(TAG, "图片保存失败", exception)
+                    runOnUiThread {
+                        handleCaptureError("拍照失败: ${exception.message}")
+                    }
+                }
+            }
+        )
+    }
+
+    private fun processCapturedImage(file: File) {
+        updateStatus("正在处理图片...")
+
+        try {
+            // 验证图片有效性
+            val bitmap = BitmapFactory.decodeFile(file.absolutePath)
+            if (bitmap == null) {
+                handleCaptureError("图片解码失败")
+                file.delete()
+                return
+            }
+
+            // 检查图片尺寸
+            if (bitmap.width < 100 || bitmap.height < 100) {
+                handleCaptureError("图片尺寸过小")
+                bitmap.recycle()
+                file.delete()
+                return
+            }
+
+            bitmap.recycle()
+
+            // 上传图片
+            uploadImageToServer(file)
+
+        } catch (e: Exception) {
+            Log.e(TAG, "图片处理失败", e)
+            handleCaptureError("图片处理失败: ${e.message}")
+            if (file.exists()) file.delete()
+        }
+    }
+
+    private fun uploadImageToServer(file: File) {
+        updateStatus("正在上传图片...")
+
+        try {
+            val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
+            val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
+
+            viewModel.uploadFile(
+                body,
+                fail = { errorMessage ->
+                    runOnUiThread {
+                        // 删除临时文件
+                        file.delete()
+                        handleRegisterResponse(
+                            success = false,
+                            errorMessage = errorMessage
+                        )
+                    }
+                },
+                success = { result ->
+                    runOnUiThread {
+                        // 删除临时文件
+                        file.delete()
+                        viewModel.faceRegistration(result!!,
+                            fail = { errorMessage ->
+                                handleRegisterResponse(
+                                    success = false,
+                                    errorMessage = errorMessage
+                                )
+                            },
+                            success = {
+                                handleRegisterResponse(
+                                    success = true,
+                                    errorMessage = null
+                                )
+                            }
+                        )
+                    }
+                }
+            )
+
+        } catch (e: Exception) {
+            Log.e(TAG, "上传失败", e)
+            runOnUiThread {
+                if (file.exists()) file.delete()
+                handleRegisterResponse(success = false, errorMessage = "上传失败: ${e.message}")
+            }
+        }
+    }
+
+    private fun handleRegisterResponse(success: Boolean, errorMessage: String?) {
+        isProcessingPhoto = false
+        isRequestInProgress = false
+        showProgressBar(false)
+        updateUIState()
+
+        // 重置人脸检测状态
+        stableFaceFrames = 0
+        isFaceDetected = false
+        isFaceQualityGood = false
+
+        if (success) {
+            // 注册成功
+            updateStatus("人脸注册成功!")
+            Toast.makeText(this, "人脸注册成功", Toast.LENGTH_SHORT).show()
+
+            // 2秒后返回个人中心
+            mainHandler.postDelayed({
+                JumpUtil.jumpActivity(RouterUrlCommon.personalCenter, this)
+                finish()
+            }, 2000)
+        } else {
+            // 注册失败
+            val attempts = captureAttempts.incrementAndGet()
+            val errorMsg = errorMessage ?: "注册失败"
+
+            if (attempts >= maxCaptureAttempts) {
+                // 达到最大尝试次数
+                updateStatus("注册失败,已达最大尝试次数")
+                Toast.makeText(this, errorMsg, Toast.LENGTH_LONG).show()
+                captureButton.isEnabled = false
+
+                // 5秒后返回
+                mainHandler.postDelayed({
+                    JumpUtil.jumpActivity(RouterUrlCommon.personalCenter, this)
+                    finish()
+                }, 5000)
+            } else {
+                // 还有机会,提示重试
+                currentAttempt++
+                updateStatus("注册失败,请重试 ($currentAttempt/$maxCaptureAttempts)")
+                Toast.makeText(this, "$errorMsg,请重试", Toast.LENGTH_SHORT).show()
+
+                // 3秒后重置状态
+                mainHandler.postDelayed({
+                    if (!isProcessingPhoto && !isRequestInProgress) {
+                        updateStatus("请将人脸对准引导框")
+                        updateCaptureButtonState(false)
+                    }
+                }, 3000)
+            }
+        }
+    }
+
+    private fun handleCaptureError(errorMessage: String) {
+        isProcessingPhoto = false
+        isRequestInProgress = false
+        showProgressBar(false)
+        updateUIState()
+
+        // 重置人脸检测状态
+        stableFaceFrames = 0
+        isFaceDetected = false
+        isFaceQualityGood = false
+
+        updateStatus("拍照失败")
+        Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()
+
+        // 2秒后重置
+        mainHandler.postDelayed({
+            if (!isProcessingPhoto && !isRequestInProgress) {
+                updateStatus("请将人脸对准引导框")
+                updateCaptureButtonState(false)
+            }
+        }, 2000)
+    }
+
+    private fun updateStatus(message: String) {
+        runOnUiThread {
+            statusTextView.text = message
+        }
+    }
+
+    private fun showProgressBar(show: Boolean) {
+        runOnUiThread {
+            progressBar.visibility = if (show) View.VISIBLE else View.GONE
+        }
+    }
+
+    private fun updateUIState() {
+        runOnUiThread {
+            if (isProcessingPhoto || isRequestInProgress) {
+                captureButton.isEnabled = false
+                captureButton.text = "处理中..."
+                captureButton.alpha = 0.6f
+            } else {
+                updateCaptureButtonState(isFaceQualityGood)
+            }
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        isProcessingPhoto = false
+        isRequestInProgress = false
+        cameraExecutor.shutdown()
+        try {
+            if (!cameraExecutor.awaitTermination(1, java.util.concurrent.TimeUnit.SECONDS)) {
+                cameraExecutor.shutdownNow()
+            }
+        } catch (e: InterruptedException) {
+            cameraExecutor.shutdownNow()
+        }
+        faceDetector.close()
+        mainHandler.removeCallbacksAndMessages(null)
+    }
+}

+ 56 - 0
personalcenter/src/main/java/com/yingyang/personalcenter/personalcenter/PersonalCenterActivity.kt

@@ -5,6 +5,7 @@ import android.text.TextUtils
 import android.view.MotionEvent
 import android.view.View
 import com.alibaba.android.arouter.facade.annotation.Route
+import com.donkingliang.imageselector.utils.StringUtils
 import com.google.android.flexbox.FlexDirection
 import com.google.android.flexbox.FlexboxLayoutManager
 import com.tencent.imsdk.v2.V2TIMCallback
@@ -14,6 +15,7 @@ import com.yingyang.personalcenter.adapter.DiagnosisAdapter
 import com.yingyang.personalcenter.databinding.ActivityPersonalCenterBinding
 import com.yingyangfly.baselib.ext.getEndAnimation
 import com.yingyangfly.baselib.ext.getScaleAnimation
+import com.yingyangfly.baselib.ext.show
 import com.yingyangfly.baselib.ext.toast
 import com.yingyangfly.baselib.mvvm.BaseMVVMActivity
 import com.yingyangfly.baselib.router.RouterUrlCommon
@@ -60,11 +62,34 @@ class PersonalCenterActivity :
             settingLayout.setOnTouchListener(this@PersonalCenterActivity)
             //退出登录
             btnLoginOut.setOnTouchListener(this@PersonalCenterActivity)
+            // 上传人脸按钮
+            humanFaceImage.setOnTouchListener(this@PersonalCenterActivity)
+            // 删除人脸按钮
+            iconDelete.setOnTouchListener(this@PersonalCenterActivity)
         }
     }
 
     override fun initData() {
         getUserInfo()
+        getHumanFaceUrl()
+    }
+
+    private fun getHumanFaceUrl() {
+        viewModel.getHumanFace(fail = {
+            it.toast()
+        }, success = {
+            runOnUiThread {
+                if (it!= null) {
+                    binding.humanFaceUrl = it
+                    binding.iconDelete.show(true)
+                }else {
+                    binding.iconDelete.show(false)
+                    binding.humanFaceImage.setImageResource(R.mipmap.icon_add_pic)
+                    // 注意:如果之前绑定过网络图片,最好也清空绑定变量,避免显示混乱
+                    binding.humanFaceUrl = null
+                }
+            }
+        })
     }
 
     /**
@@ -157,6 +182,19 @@ class PersonalCenterActivity :
                     R.id.btnLoginOut -> {
                         loginOut()
                     }
+                    R.id.humanFaceImage ->{
+                        if (binding.humanFaceUrl == null) {
+                            JumpUtil.jumpActivity(RouterUrlCommon.faceRegister, mContext)
+                        }else {
+                            "请先删除旧的人脸照片".toast()
+                        }
+
+                    }
+                    R.id.iconDelete -> {
+                        if (binding.humanFaceUrl != null) {
+                            deleteHumanFace(binding.humanFaceUrl!!)
+                        }
+                    }
                 }
             }
 
@@ -169,6 +207,24 @@ class PersonalCenterActivity :
         return true
     }
 
+
+    private fun deleteHumanFace(url:String) {
+        if (StringUtils.isNotEmptyString(url)){
+            viewModel.deleteHumanFace(url, fail = {
+                "删除失败".toast()
+            }, success = {
+                if (it!!) {
+                    "删除成功!".toast()
+                    getHumanFaceUrl()
+                }else {
+                    "删除失败".toast()
+                }
+            })
+        }else{
+            "删除失败".toast()
+        }
+    }
+
     /**
      * 退出登录
      */

+ 46 - 2
personalcenter/src/main/java/com/yingyang/personalcenter/personalcenter/PersonalCenterViewModel.kt

@@ -3,6 +3,9 @@ package com.yingyang.personalcenter.personalcenter
 import com.yingyang.personalcenter.net.PERSONALCENTER_API
 import com.yingyangfly.baselib.bean.UserInfoBean
 import com.yingyangfly.baselib.mvvm.BaseViewModel
+import com.yingyangfly.baselib.utils.FileMultipartBuilder
+import okhttp3.MultipartBody
+import java.io.File
 
 /**
  * 个人中心
@@ -11,7 +14,7 @@ class PersonalCenterViewModel : BaseViewModel() {
 
     fun getUserInfo(
         fail: ((msg: String) -> Unit)? = null,
-        success: ((success: UserInfoBean?) -> Unit)? = null,
+        success: ((success: UserInfoBean?) -> Unit)? = null
     ) = launchFlow(true) {
         PERSONALCENTER_API.getUserInfo()
     }.runUI(
@@ -21,11 +24,52 @@ class PersonalCenterViewModel : BaseViewModel() {
 
     fun logout(
         fail: ((msg: String) -> Unit)? = null,
-        success: ((success: String?) -> Unit)? = null,
+        success: ((success: String?) -> Unit)? = null
     ) = launchFlow(true) {
         PERSONALCENTER_API.logout()
     }.runUI(
         success,
         fail
     )
+
+    fun uploadFile(
+        multipartBody: MultipartBody.Part,
+        fail: ((msg: String) -> Unit)? = null,
+        success: ((success: String?) -> Unit)? = null
+    ) = launchFlow(false) {
+        PERSONALCENTER_API.uploadFile("humanFace", multipartBody)
+    }.runUI(
+        success,
+        fail
+    )
+
+
+    fun faceRegistration(
+        humanFaceUrl: String,
+        fail: ((msg: String) -> Unit)? = null,
+        success: ((success: String?) -> Unit)? = null
+    ) = launchFlow(true) {
+        PERSONALCENTER_API.faceRegistration(humanFaceUrl)
+    }.runUI(
+        success, fail
+    )
+
+    fun getHumanFace(
+        fail: ((msg: String) -> Unit)? = null,
+        success: ((success: String?) -> Unit)? =null
+    ) = launchFlow {
+        PERSONALCENTER_API.getHumanFace()
+    }.runUI(
+        success,fail
+    )
+
+    fun deleteHumanFace(
+        faceUrl: String,
+        fail: ((msg: String) -> Unit)? = null,
+        success: ((success: Boolean?) -> Unit)? =null
+    ) = launchFlow {
+        PERSONALCENTER_API.deleteHumanFace(faceUrl)
+    }.runUI(
+        success,fail
+    )
 }

+ 11 - 1
personalcenter/src/main/manifest/AndroidManifest.xml

@@ -2,9 +2,14 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.yingyang.personalcenter">
 
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="28" /> <!-- Android 9及以下 -->
+
     <application
         android:allowBackup="true"
-        android:supportsRtl="true">
+        android:supportsRtl="true"
+        android:requestLegacyExternalStorage="true">
         <activity
             android:name=".version.VersionRecordActivity"
             android:configChanges="keyboardHidden|orientation|screenSize"
@@ -62,6 +67,11 @@
             android:configChanges="keyboardHidden|orientation|screenSize"
             android:screenOrientation="landscape"
             android:windowSoftInputMode="adjustResize|adjustPan" />
+        <activity
+            android:name=".personalcenter.FaceRegisterActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="landscape"
+            android:windowSoftInputMode="adjustResize|adjustPan" />
     </application>
 
 </manifest>

+ 6 - 0
personalcenter/src/main/res/drawable/btn_primary.xml

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

+ 5 - 0
personalcenter/src/main/res/drawable/circle_background.xml

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

+ 9 - 0
personalcenter/src/main/res/drawable/face_guide_frame.xml

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

+ 75 - 0
personalcenter/src/main/res/layout/activity_face_register_simple.xml

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/black">
+
+    <!-- 相机预览 -->
+    <androidx.camera.view.PreviewView
+        android:id="@+id/preview_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <!-- 引导框 -->
+    <View
+        android:id="@+id/overlay_view"
+        android:layout_width="240dp"
+        android:layout_height="240dp"
+        android:layout_centerInParent="true"
+        android:background="@drawable/face_guide_frame" />
+
+    <!-- 顶部返回按钮 -->
+    <ImageView
+        android:id="@+id/icon_back"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginTop="50dp"
+        android:layout_marginLeft="16dp"
+        android:src="@android:drawable/ic_menu_close_clear_cancel"
+        android:background="@drawable/circle_background"
+        android:padding="12dp"
+        android:contentDescription="返回" />
+
+    <!-- 底部控制区域 -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:orientation="vertical"
+        android:gravity="center"
+        android:padding="20dp"
+        android:background="#80000000">
+
+        <!-- 状态显示 -->
+        <TextView
+            android:id="@+id/status_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@android:color/white"
+            android:textSize="16sp"
+            android:padding="8dp"
+            android:text="请正对摄像头" />
+
+        <!-- 进度条 -->
+        <ProgressBar
+            android:id="@+id/progress_bar"
+            style="?android:attr/progressBarStyleHorizontal"
+            android:layout_width="200dp"
+            android:layout_height="4dp"
+            android:layout_marginVertical="8dp"
+            android:visibility="gone" />
+
+        <!-- 拍照按钮 -->
+        <Button
+            android:id="@+id/capture_button"
+            android:layout_width="160dp"
+            android:layout_height="48dp"
+            android:layout_marginTop="16dp"
+            android:background="@drawable/btn_primary"
+            android:textColor="@android:color/white"
+            android:text="拍照"
+            android:textSize="16sp" />
+
+    </LinearLayout>
+
+</RelativeLayout>

+ 41 - 0
personalcenter/src/main/res/layout/activity_personal_center.xml

@@ -9,6 +9,10 @@
         <variable
             name="data"
             type="com.yingyangfly.baselib.bean.UserInfoBean" />
+
+        <variable
+            name="humanFaceUrl"
+            type="String" />
     </data>
 
     <LinearLayout
@@ -229,6 +233,43 @@
                     app:layout_constraintStart_toEndOf="@id/tvDiagnosis"
                     app:layout_constraintTop_toBottomOf="@+id/tvHospital"
                     tools:listitem="@layout/item_diagnosis" />
+
+
+                <TextView
+                    android:id="@+id/tvHumanFace"
+                    style="@style/layout_properties_self_adaption"
+                    android:layout_marginStart="@dimen/divider_31px"
+                    android:layout_marginTop="@dimen/divider_38px"
+                    android:text="@string/human_face_image"
+                    android:textColor="@color/color_FFFFFF"
+                    android:textSize="@dimen/divider_30px"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@+id/tvDiagnosis" />
+
+                <androidx.appcompat.widget.AppCompatImageView
+                    android:id="@+id/humanFaceImage"
+                    android:layout_width="@dimen/divider_160px"
+                    android:layout_height="@dimen/divider_160px"
+                    android:scaleType="centerCrop"
+                    android:layout_marginStart="@dimen/divider_30px"
+                    android:layout_marginTop="@dimen/divider_30px"
+                    app:layout_constraintStart_toEndOf="@id/tvHumanFace"
+                    app:layout_constraintTop_toBottomOf="@+id/tvDiagnosis"
+                    app:loadImg="@{humanFaceUrl}"
+                    />
+
+                <ImageView
+                    android:id="@+id/iconDelete"
+                    android:layout_width="10dp"
+                    android:layout_height="10dp"
+                    android:src="@android:drawable/ic_menu_close_clear_cancel"
+                    android:background="@drawable/circle_background"
+                    android:padding="2dp"
+                    app:layout_constraintTop_toTopOf="@id/humanFaceImage"
+                    app:layout_constraintEnd_toEndOf="@id/humanFaceImage"
+                    android:layout_marginTop="-5dp"
+                    android:layout_marginEnd="-5dp"
+                    />
             </androidx.constraintlayout.widget.ConstraintLayout>
 
         </androidx.core.widget.NestedScrollView>

BIN
personalcenter/src/main/res/mipmap-xxhdpi/icon_add_pic.png


BIN
personalcenter/src/main/res/mipmap-xxhdpi/icon_delect_image.png


+ 6 - 0
personalcenter/src/main/res/values/strings.xml

@@ -55,4 +55,10 @@
     <string name="equipment_lease_agreement" tools:ignore="ResourceName">服务协议</string>
     <string name="renewal_fee_standard" tools:ignore="ResourceName">医疗器械产品续费收费标准</string>
     <string name="compensation_price_list" tools:ignore="ResourceName">赔偿标准价目表</string>
+    <string name="human_face_image" tools:ignore="ResourceName">人脸照片:</string>
+<!--    <string name="capture_face_image">拍摄人脸照片</string>-->
+<!--    <string name="face_image_capture">人脸照片拍摄</string>-->
+<!--    <string name="capturing_face">正在拍摄人脸...</string>-->
+<!--    <string name="face_capture_success">人脸照片拍摄成功</string>-->
+<!--    <string name="face_capture_failed">人脸照片拍摄失败</string>-->
 </resources>