package com.sw.dualscreen.presentation import android.Manifest import android.annotation.SuppressLint import android.app.Presentation import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Point import android.hardware.Camera import android.os.Bundle import android.util.Log import android.view.Display import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.postDelayed import androidx.core.view.updateLayoutParams import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.arcsoft.face.ErrorInfo import com.sw.dualscreen.BuildConfig import com.sw.dualscreen.GlobalKey import com.sw.dualscreen.R import com.sw.dualscreen.activity.MainActivity import com.sw.dualscreen.adapter.FoodOrderAdapter import com.sw.dualscreen.databinding.PresentationMainScreenBinding import com.sw.dualscreen.ext.dp import com.sw.dualscreen.ext.format2String import com.sw.dualscreen.ext.gone import com.sw.dualscreen.ext.load import com.sw.dualscreen.ext.maskName import com.sw.dualscreen.ext.roundedDecimalPlace import com.sw.dualscreen.ext.screenSize import com.sw.dualscreen.ext.visible import com.sw.dualscreen.model.response.DinnerType import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodItem import com.sw.dualscreen.model.response.UserEnergy import com.sw.dualscreen.model.response.UserNutrition import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.SPUtil import com.sw.dualscreen.utils.countDownByFlow import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.plate.utils.LightManager import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.arcface.ConfigUtil import com.sw.plate.utils.arcface.ErrorCodeUtil import com.sw.plate.utils.arcface.FaceRectTransformer import com.sw.plate.utils.arcface.FaceRectView import com.sw.plate.utils.arcface.FaceRectView.DrawInfo import com.sw.plate.utils.arcface.PreviewConfig import com.sw.plate.utils.arcface.camera.CameraListener import com.sw.plate.utils.arcface.camera.DualCameraHelper import com.sw.plate.utils.arcface.face.constants.LivenessType import com.sw.plate.utils.arcface.face.model.CompareResult import com.sw.plate.utils.arcface.face.model.FacePreviewInfo import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import timber.log.Timber import java.text.DecimalFormat import kotlin.math.max import kotlin.math.roundToInt class MainScreenPresentation( val activity: MainActivity, display: Display, val userViewModel: UserViewModel, val recognizeViewModel: RecognizeViewModel, private val onDismissListener: () -> Unit = {} ) : Presentation(activity, display), ViewTreeObserver.OnGlobalLayoutListener { companion object { const val TAG = "MainScreenPresentation" } private lateinit var binding: PresentationMainScreenBinding private lateinit var stepChangeCallback: (Int) -> Unit // 当前步骤 var currentStep: Int = 0 private var isGoStep1 = false;//防止持续调用 // 虹软人脸配置 ⬇ private var isRecognition = false private var rgbCameraHelper: DualCameraHelper? = null private var rgbFaceRectTransformer: FaceRectTransformer? = null private val livenessType = LivenessType.IR private var openRectInfoDraw = false private var irCameraHelper: DualCameraHelper? = null private var irFaceRectTransformer: FaceRectTransformer? = null private var detectWeight = 0.0 //识别菜品时的重量 var lastWeight = 0 // 上一次的计算热量结果 // private var userNutritionData: UserNutritionData? = null private var userNutrition: UserNutrition? = null private var dinnerTypeInfo: DinnerType? = null private val debouncer = Debouncer(1500) private var recognitionTime: Long = 0L // 人脸识别时的时间 var recognitionWeight: Int = 0 // 人脸识别时的重量 private var lastFaceTrackId: Int = -1 // 上一次的人脸信息 var mealPickupMode: Int = 0 // 取餐模式 0 即放即取 1 余量取餐 // var isKeepFaceState = true var isKeepFaceState = false var currentUserId: String? = null var isMember: Boolean = false var facePreviewInfoList: MutableList? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = PresentationMainScreenBinding.inflate(layoutInflater) setContentView(binding.root) //0-即放即取,1-余量计量 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 window?.setBackgroundDrawableResource(android.R.color.white) initView() //0-计费,1-不计费 val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) if (activity.settlementMode == 0 || chargeMode == 1) { setupArcCamera() } registerDataChange() startTime = System.currentTimeMillis() } private var isFirstGetWeight = true fun updateWeight(weight: Int) { lastWeight = weight Timber.tag(TAG) .d("updateWeight lastWeight = $weight, currentStep = $currentStep, isGoStep1 = $isGoStep1") //0-即放即取,1-余量计量 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 //0-计费,1-不计费 val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) if (currentStep == 2 && lastWeight <= MainActivity.WEIGHT_RESET_RECOGNIZE && isGoStep1) { //人脸识别过程中,秤上物品拿走 isGoStep1 = false activity.runOnUiThread { Timber.tag(TAG).e("切回step1,------2000000000000") step1FoodRecognizing() } } else if (lastWeight <= MainActivity.WEIGHT_RESET_RECOGNIZE) { isGoStep1 = false activity.runOnUiThread { Timber.tag(TAG).e("切回step1,------2000000000001") if (mealPickupMode == 0) { //即放即取 step1FoodRecognizing() } else { if (currentStep == 1 && currentFood == null) { return@runOnUiThread } //余量计量 if (countDownResetJob == null) { countDownResetJob = countDownByFlow( total = 30, scope = activity.lifecycleScope, onStart = {}, onTick = { seconds -> Timber.tag(TAG).e("余量计量模型秤重量已清空,等待${seconds}秒后继续重置识别状态") if (lastWeight > MainActivity.WEIGHT_RESET_RECOGNIZE) { countDownResetJob?.cancel() } }, onFinish = { step1FoodRecognizing() countDownResetJob?.cancel() } ) } } } } if (currentFood == null) { Timber.tag(TAG).e("updateWeight currentFood is null") return } if (userNutrition == null) { Timber.tag(TAG).e("updateWeight userNutritionData is null") return } if (chargeMode == 1 || activity.settlementMode == 0) { //不计费或者联合支付模式更新营养热量数据 if (mealPickupMode == 0) { //即放即取 calculateNutrition(lastWeight) } else { //余量计量 calculateNutrition(recognitionWeight - lastWeight) } } } private var countDownResetJob: Job? = null fun setStepChangeCallback(callback: (Int) -> Unit) { stepChangeCallback = callback } override fun onGlobalLayout() { Timber.tag(TAG).d("onGlobalLayout") binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this) openCamera() } private fun openCamera() { Timber.tag(TAG).d("openCamera") if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ) { ToastUtils.showToast("无摄像头权限") return } try { val cameraCount = Camera.getNumberOfCameras() if (cameraCount < 3) { ToastUtils.showToast("摄像头数量异常") return } recognizeViewModel.init( PreviewConfig( 2, 1, 90, 90 ) ) initRgbCamera() if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) { initIrCamera() } } catch (e: Exception) { Timber.tag(TAG).e(e, "打开摄像头失败") } } /** * 启用虹软人脸失败 */ fun setupArcCamera() { initArcViewModel() initArcView() openRectInfoDraw = true } private fun registerDataChange() { activity.lifecycleScope.launch { userViewModel.loadFaceResult.collect { needUpdate -> if (needUpdate) { recognizeViewModel.refreshFaceList() } } } // activity.lifecycleScope.launch { // userViewModel.nutritionData.collect { // Timber.tag(TAG).d("registerDataChange nutritionData = $it") // userNutritionData = it // if (it == null) return@collect //// step3ShowRecognizeResult() // //binding.nutritionInclude.tvUserName.text = it.userName.maskName() // //binding.nutritionInclude.tvRecommendHeat.text = "推荐热量:${it.recommendMin}-${it.recommendMax}" //// //updateWeight(lastWeight / 1000) // } // } activity.lifecycleScope.launch { userViewModel.dinnerTypeInfo.drop(1).collect { Timber.tag(TAG).d("registerDataChange dinnerTypeInfo = $it") dinnerTypeInfo = it } } } private fun setTopInfoVisible() { binding.llTopInfo.visible() binding.flFoodDetail.gone() if (activity.settlementMode == 0) { try { foodOrderList.clear() foodOrderAdapter.submitList(foodOrderList) } catch (e: Exception) { e.printStackTrace() } } binding.llTopInfo.updateLayoutParams { height = activity.screenSize[1] / 2 + 120.dp } } private var isGoRecognize = true /** * 显示识别出的菜品信息 */ fun step3ShowRecognizeResult(userId: String) { step3Debouncer.debounce { Timber.tag(TAG).d("main,step3,耗时:${System.currentTimeMillis() - startTime}") LightManager.closeRedLight() LightManager.openGreenLight() currentStep = 3 stepChangeCallback(currentStep) setTopInfoVisible() if (activity.settlementMode == 0) { //判断是联合支付调用接口查询就餐信息后,再查询热量营养数据 loadOrderList(userId) { loadUserNutritionData(userId) } } else { //独立支队 loadUserNutritionData(userId) } step3Debouncer.reset() } } private val step3Debouncer = Debouncer(10*1000) /** * 根据接口数据更新热量、营养数据 */ private fun loadUserNutritionData(userId: String) { userViewModel.getUserNutritionData(userId = userId) { nutrition -> activity.runOnUiThread { if (nutrition == null) { activity.hideWaitingDialog() ToastUtils.showToast("未查询到营养数据") return@runOnUiThread } this@MainScreenPresentation.userNutrition = nutrition loadUnbilledMode() val imageUrl = if (currentFood!!.foodImg.isNullOrBlank()) currentFood!!.photoUri else currentFood!!.foodImg binding.ivRecImage.let { it.visible() it.load(imageUrl) } activity.updateImage(imageUrl) activity.hideWaitingDialog() } } } private fun initView() { // binding.ivRecImage.let { // it.outlineProvider = object : ViewOutlineProvider() { // override fun getOutline(view: View, outline: Outline) { // outline.setRoundRect(0, 0, view.width, view.height, 12f.dp) // } // } // it.clipToOutline = true // } binding.ivPreviewImage.visible() } override fun onDisplayRemoved() { super.onDisplayRemoved() onDismissListener() } /** * 加载待机页面 */ fun loadStandbyScreen() { binding.llStandbyPage.visible() } fun hideStandbyScreen() { binding.llStandbyPage.gone() } /** * 餐品识别成功,计费模式 */ fun loadBilledMode() { binding.calorieInclude.root.visible() binding.nutritionInclude.root.gone() binding.flCameraView.visible() binding.ivFaceRecMask.gone() binding.llCircleArea.gone() binding.tvFaceTip.gone() binding.tvFoodRecPrompt.gone() binding.llPriceInfo.visible() //binding.ivRecImage.gone() //updateFoodInfo(foodInfo) // userNutritionData?.let { // Timber.tag(TAG).d("loadBilledMode:${GsonUtils.toJson(it)}") // } currentFood?.let { val specPrice = it.specPrice ?: 0.0 val vipPrice = it.vipPrice ?: 0.0 binding.tvNormalPrice.text = "${specPrice.format2String(2)} 元/份" binding.tvVipPrice.text = "${vipPrice.format2String(2)} 元/份" var calorie = getIntNutritionValue(it.calorie) calorie = max(calorie, 0) val fat = getIntNutritionValue(it.fat) val protein = getIntNutritionValue(it.protein) val carbohydrate = getIntNutritionValue(it.carbohydrate) val fatRate = getNutritionRate(fat, calorie) val proteinRate = getNutritionRate(protein, calorie) val carbohydrateRate = getNutritionRate(carbohydrate, calorie) binding.calorieInclude.run { tvCalorie.text = "$calorie" tvFatRate.text = "$fatRate%" tvProteinRate.text = "$proteinRate%" tvCarbohydrateRate.text = "$carbohydrateRate%" viewFatLine.updateLayoutParams { height = (fatRate / 100.0 * 84).roundToInt() * 1.dp } viewProteinLine.updateLayoutParams { height = (proteinRate / 100.0 * 84).roundToInt() * 1.dp } viewCarbohydrateLine.updateLayoutParams { height = (carbohydrateRate / 100.0 * 84).roundToInt() * 1.dp } } } } private fun getNutritionRate(value: Int, total: Int): Int { if (value == 0 || total == 0) return 0 return (100.0 * value / total).roundToInt() } private fun getIntNutritionValue(num: Double?): Int { return num?.roundToInt() ?: 0 } private var isLoadUnbilledMode = false // private val df by lazy { DecimalFormat("#.##") } /** * 餐品识别成功,不计费模式 */ fun loadUnbilledMode() { if (currentUserId.isNullOrBlank()) { return } //todo 清除人脸数据 // binding.dualCameraFaceRectView.clearFaceInfo() // clearLeftFaceData() // facePreviewInfoList?.clear() // recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY) // pauseCamera() // resumeCamera() // TODO: 如何清除人脸数据重新识别 // stopCamera() // setupArcCamera() isLoadUnbilledMode = true Timber.tag(TAG).d("loadUnbilledMode") binding.calorieInclude.root.gone() binding.nutritionInclude.root.visible() binding.ivPreviewImage.gone() binding.flCameraView.visible() binding.ivFaceRecMask.gone() binding.tvFaceTip.gone() binding.llCircleArea.gone() binding.tvFoodRecPrompt.gone() binding.llPriceInfo.gone() //binding.ivRecImage.gone() //updateFoodInfo(foodInfo) binding.nutritionInclude.tvUserName.text = userNutrition?.name.maskName() val recommendCalorie = (userNutrition?.recommendCalorie ?: 0.0).roundedDecimalPlace(1) binding.nutritionInclude.tvRecommendHeat.text = "推荐热量:${recommendCalorie.roundedDecimalPlace(1)}kcal" // binding.nutritionInclude.tvRecommendHeat.text = "推荐热量:${recommendCalorie.removeTrailingZeros()}kcal" //updateWeight(lastWeight / 1000) // ------------------------------------------------- // 删除userNutritionData,改为userNutrition //0-即放即取,1-余量计量 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 calculateNutrition( if (mealPickupMode == 1) recognitionWeight - lastWeight else recognitionWeight ) binding.root.postDelayed({ isLoadUnbilledMode = false }, 10000) if (isAgainLoadData.not()) { //上次已经识别过人脸 step3ShowRecognizeResult(currentUserId!!) isAgainLoadData = true } } public var isAgainLoadData = false private var isFirstOpen = true var startTime = 0L var jointPaymentQueryFinish = false /** * 餐品识别中 */ fun step1FoodRecognizing() { if (currentStep == 1 && currentFood == null) { return } Timber.tag(TAG).d("main,step1,耗时:${System.currentTimeMillis() - startTime}") startTime = System.currentTimeMillis() jointPaymentQueryFinish = false hideStandbyScreen() activity.resetTouchTime() if (isFirstOpen.not()) { activity.resumeAnalysis() } //if (abs(lastWeight.toInt()) <= 5) { // activity.runOnUiThread { // ToastUtils.showToast("秤上重量几乎为0,请确认") // } // return //} LightManager.closeGreenLight() LightManager.closeRedLight() dinnerTypeInfo = null currentFood = null // userNutritionData = null userNutrition = null recognitionWeight = 0 setTopInfoVisible() binding.calorieInclude.root.gone() binding.nutritionInclude.root.gone() binding.tvFoodName.text = "餐品识别中..." binding.tvFoodRecPrompt.visible() binding.flCameraView.visible() binding.ivRecImage.gone() binding.ivPreviewImage.visible() binding.ivFaceRecMask.gone() binding.tvFaceTip.gone() binding.llCircleArea.gone() binding.llPriceInfo.gone() binding.flFace.gone() binding.ivFrame.gone() binding.flRecognizeIr.gone() pauseCamera() activity.clearRecognizeFood() currentStep = 1 stepChangeCallback(currentStep) } // fun foodRecSuccess(foodInfo: FoodInfo) { // //识别逻辑完成根据是否计费显示不同页面 // val mode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) // if (mode == 0) { // //计费 // loadBilledMode() // } else { // //不计费 // //userViewModel.getUserNutritionData( // // userId = userId, // // foodId = currentFood!!.foodId!! // //) // loadUnbilledMode() // } // } /** * 用户人脸识别中 */ fun step2FaceRecognizing(foodInfo: FoodInfo, hideFoodDetail: Boolean = false) { isGoStep1 = true Timber.tag(TAG).d("main,step2,耗时:${System.currentTimeMillis() - startTime}") if (hideFoodDetail) { setTopInfoVisible() } startTime = System.currentTimeMillis() currentStep = 2 LightManager.closeRedLight() binding.calorieInclude.root.gone() binding.nutritionInclude.root.gone() binding.tvFoodName.text = foodInfo.foodName binding.flFace.visible() binding.flRecognizeIr.visible() binding.flCameraView.visible() binding.ivFaceRecMask.visible() binding.tvFaceTip.visible() binding.llCircleArea.visible() binding.tvFoodRecPrompt.visible() binding.llPriceInfo.gone() binding.ivRecImage.gone() binding.ivPreviewImage.gone() binding.ivFrame.gone() dinnerTypeInfo = null // userNutritionData = null userNutrition = null recognitionWeight = 0 stepChangeCallback(currentStep) currentFood = foodInfo // userViewModel.getDinnerType() resumeCamera() } fun hideRecImage() { binding.ivRecImage.gone() } // 当前食物信息 var currentFood: FoodInfo? = null fun updateFood(foodInfo: FoodInfo?) { //if (isLoadUnbilledMode) return Timber.tag(TAG).d("main,updateFood,耗时:${System.currentTimeMillis() - startTime}") startTime = System.currentTimeMillis() currentFood = foodInfo if (foodInfo == null) { return } binding.tvFoodName.text = foodInfo.foodName binding.ivPreviewImage.gone() binding.ivFrame.gone() //0-计费,1-不计费 val chargeMode = SPUtil.getInstance().get(GlobalKey.KEY_CHARGE_MODE, 0) if (chargeMode == 0) { binding.ivRecImage.let { it.visible() val imageUrl = if (foodInfo.foodImg.isNullOrBlank()) foodInfo.photoUri else foodInfo.foodImg it.load(imageUrl) } } else { binding.ivRecImage.gone() } //独立支付+计费模式,使用人脸识别 if (activity.settlementMode == 1 && chargeMode == 0) { //计费,查询营养信息、价格信息------------------------ loadBilledMode() } else { //联合支付,进行人脸识别 if (activity.settlementMode == 0) { step2FaceRecognizing(foodInfo) } else { //不计费,开启人脸识别 if (currentStep == 1 || binding.tvFaceTip.isVisible.not()) { step2FaceRecognizing(foodInfo) } } } //if (currentStep == 1) { // step2FaceRecognizing(foodInfo) //} else { // updateFoodInfo(foodInfo) //} } private fun updateFoodInfo(foodInfo: FoodInfo) { // binding.tvFoodName.text = foodInfo.foodName // binding.ivPreviewImage.gone() //binding.ivRecImage.visible() // if (foodInfo.photoUri != null) { // GlideUtils.loadRoundCornerImage( // context, // url = foodInfo.photoUri, // imageView = //binding.ivRecImage, // radius = 12 // ) // } else { // GlideUtils.loadRoundCornerImage( // context, // url = foodInfo.imgUrl, // imageView = //binding.ivRecImage, // radius = 12 // ) // } } private var lastAddWeight = 0 private fun addFoodToOrder(weight: Int, foodInfo: FoodInfo) { Log.d(TAG, "addFoodToOrder: ${weight == lastAddWeight}") // if (weight == lastAddWeight) return lastAddWeight = weight if (weight <= 10) { //0-即放即取,1-余量计量 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 if (foodOrderList.isNotEmpty() && mealPickupMode == 1) { val last = foodOrderList.last() if (last.isLocalData) { foodOrderList.remove(last) // foodOrderAdapter.submitList(foodOrderList) foodOrderAdapter.notifyDataSetChanged() } } return } val foodPrice = if (isMember) foodInfo.vipPrice else foodInfo.specPrice val eatNum = activity.getEatNum(weight.toDouble(), foodInfo.specWeight) val price = eatNum * (foodPrice ?: 0.0) if (foodOrderList.isNotEmpty() && mealPickupMode == 1) { val last = foodOrderList.last() if (last.isLocalData) { last.eatNum = eatNum last.num = weight last.price = price // foodOrderAdapter.submitList(foodOrderList) foodOrderAdapter.notifyItemChanged(foodOrderList.lastIndex) return } } foodOrderList.add( FoodItem( id = System.currentTimeMillis().toString(), foodId = foodInfo.foodId, foodName = foodInfo.foodName, price = price, specId = foodInfo.specId, foodMaterialId = foodInfo.foodMaterialId, specWeight = foodInfo.specWeight?.toInt() ?: 0, orderFrom = 2, num = weight, eatNum = eatNum, isLocalData = true ) ) // foodOrderAdapter.submitList(foodOrderList) foodOrderAdapter.notifyDataSetChanged() Log.d( TAG, "addFoodToOrder: size=${foodOrderList.size},新增数据:${foodOrderList.last()}" ) //foodOrderAdapter.notifyDataSetChanged() var eatWeightSum = 0 foodOrderList.forEach { eatWeightSum += it.num } binding.detailInclude.tvTotalWeight.text = "总重量:${eatWeightSum}克" Log.d(TAG, "addFoodToOrder: 更新总重量,新增数据------------------------------") } private fun calculateNutrition(value: Int) { Timber.tag(TAG) .d("calculateNutrition:weight=$value,recognitionWeight=$recognitionWeight,foodInfo=$currentFood") if (currentFood == null || userNutrition == null) { return } var weight = value if (weight < 0) weight = 0 if (activity.settlementMode == 0) { activity.runOnUiThread { addFoodToOrder(weight, currentFood!!) } } debouncer.debounce { Timber.tag(TAG) .d("calculateNutrition weight = $weight, recognitionWeight = $recognitionWeight") val dinnerType = dinnerTypeInfo?.dinnerType // Timber.tag(TAG) // .d("calculateNutrition foodName = ${currentFood!!.foodName}, dinnerType = $dinnerType, userId = ${userNutritionData!!.userId}") val energy: UserEnergy = UserNutritionUtils.calculateNutrition2( currentFood!!, // userNutritionData!!, userNutrition!!, weight.toDouble(), dinnerType = dinnerType ?: "" ) // Timber.tag(TAG).d("calculateNutrition calcResultInfo = $calcResultInfo") // val columnMax = UserNutritionUtils.getMaxInt(calcResultInfo) var columnMax = max(energy.grain, energy.fruitsVegetables) columnMax = max(columnMax, energy.meatEggs) activity.runOnUiThread { val totalKcal = energy.calorie val grain = energy.grain val fruitsVegetables = energy.fruitsVegetables val meatEggs = energy.meatEggs binding.detailInclude.tvTotalCalorie.text = "总热量:${totalKcal.roundedDecimalPlace(1)}千卡" val totalWeight = (userNutrition?.eatWeightSum ?: 0.0) + weight binding.detailInclude.tvTotalWeight.text = "总重量:${totalWeight.roundedDecimalPlace(1)}克" val include = binding.nutritionInclude // 设置热量--------------------------------------------------------------------- val calorieArray = doubleArrayOf( 0.0, userNutrition?.minCalorie ?: 0.0, userNutrition?.recommendCalorie ?: 0.0, ((userNutrition?.recommendCalorie ?: 0.0) + (userNutrition?.maxCalorie ?: 0.0)) / 2, userNutrition?.maxCalorie ?: 0.0, ) val calorieIndex = UserNutritionUtils.findCalorieIndex(energy.calorie, calorieArray) include.customKcalColumn.setImageDrawable(true, calorieIndex) var maxKcal = userNutrition?.maxCalorie ?: 1.0 Timber.tag(TAG).d("maxKcal = $maxKcal") // 避免0作为被除数 maxKcal = if (maxKcal <= 0.0) 1.0 else maxKcal Timber.tag(TAG).d("maxKcal 1 = $maxKcal") include.totalKcalTv.text = totalKcal.roundedDecimalPlace(1).toString() var totalKcalHeight = (totalKcal / maxKcal).toFloat() if (totalKcalHeight == 0F) { include.divKcalView.visible() } else { include.divKcalView.gone() totalKcalHeight += 0.1F } include.customKcalColumn.setCustomHeightPercent(totalKcalHeight, true) // 设置主食----------------------------------------------------------- val grainArray = UserNutritionUtils.getCalorieArray( left = userNutrition?.stapleFoodRecommend, mid = userNutrition?.stapleFoodNearExcess, right = userNutrition?.stapleFoodExcess ) val grainIndex = UserNutritionUtils.findCalorieIndex(energy.grain, grainArray) include.customFoodColumn.setImageDrawable(false, grainIndex) include.totalFoodTv.text = grain.roundedDecimalPlace(1).toString() columnMax = if (columnMax <= 0.0) 1.0 else columnMax var grainHeight = (grain / columnMax).toFloat() if (grainHeight == 0F) { include.divTotalFoodView.visible() } else { include.divTotalFoodView.gone() grainHeight += 0.1F } include.customFoodColumn.setCustomHeightPercent(grainHeight, true) // 设置果蔬------------------------------------------------------------- val fruitsArray = UserNutritionUtils.getCalorieArray( left = userNutrition?.fruitsVegetablesRecommend, mid = userNutrition?.fruitsVegetablesNearExcess, right = userNutrition?.fruitsVegetablesExcess ) val fruitsIndex = UserNutritionUtils.findCalorieIndex(energy.fruitsVegetables, fruitsArray) include.customVegetableColumn.setImageDrawable(false, fruitsIndex) include.totalVegetableTv.text = fruitsVegetables.roundedDecimalPlace(1).toString() var fruitsHeight = (fruitsVegetables / columnMax).toFloat() if (fruitsHeight == 0F) { include.divVegetableView.visible() } else { include.divVegetableView.gone() fruitsHeight += 0.1F } include.customVegetableColumn.setCustomHeightPercent(fruitsHeight, true) // 设置肉蛋----------------------------------------------------------- val meatEggsArray = UserNutritionUtils.getCalorieArray( left = userNutrition?.meatEggsBeansRecommend, mid = userNutrition?.meatEggsBeansNearExcess, right = userNutrition?.meatEggsBeansExcess ) val meatEggsIndex = UserNutritionUtils.findCalorieIndex(energy.meatEggs, meatEggsArray) include.customMeatColumn.setImageDrawable(false, meatEggsIndex) include.totalMeatTv.text = meatEggs.roundedDecimalPlace(1).toString() var meatHeight = (meatEggs / columnMax).toFloat() if (meatHeight == 0F) { include.divMeatView.visible() } else { include.divMeatView.gone() meatHeight += 0.1F } include.customMeatColumn.setCustomHeightPercent(meatHeight, true) } } } fun updateImage(bitmap: Bitmap) { activity.runOnUiThread { binding.ivPreviewImage.setImageBitmap(bitmap) } } /** * 余量取餐 */ fun updateMealPickupMode(mode: Int) { Timber.tag(TAG).d("updateMealPickupMode mode = $mode") Timber.tag(TAG).e("切回step1,------2000000000003") mealPickupMode = mode step1FoodRecognizing() } fun stopCamera() { rgbCameraHelper?.release() rgbCameraHelper = null irCameraHelper?.release() irCameraHelper = null recognizeViewModel.destroy() } public override fun onStop() { stopCamera() super.onStop() } fun resumeCamera() { Timber.tag(TAG).d("resumeCamera isRecognition = $isRecognition") isRecognition = true if (rgbCameraHelper == null) { Timber.tag(TAG).d("resumeCamera isRecognition ---1") setupArcCamera() binding.root.postDelayed({ if (rgbCameraHelper != null) { rgbCameraHelper!!.start() } }, 2000) } else if (rgbCameraHelper!!.isStopped) { Timber.tag(TAG).d("resumeCamera isRecognition ---2") rgbCameraHelper!!.start() } } fun pauseCamera() { Timber.tag(TAG).d("pauseCamera isRecognition = $isRecognition") isRecognition = false recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) //triggerReidentify() } /** * 重置人脸跟踪状态,让已在画面中的人脸重新触发识别 * 适用场景:切换按钮后需要重新识别当前画面中的人脸 */ fun triggerReidentify() { // 清除UI上的人脸框 binding.dualCameraFaceRectView.clearFaceInfo() // 清除本地跟踪数据 clearLeftFaceData() // 重置上一次的 trackId,避免 drawPreviewInfo 中的去重逻辑拦截 lastFaceTrackId = -1 // 插入空帧,让引擎重置内部跟踪状态 // isRecognition 保持 true,相机继续送帧,下一帧真实人脸会产生新 trackId recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) } private var recognizeTime = 0L private fun initArcViewModel() { recognizeViewModel.setLiveType(livenessType) recognizeViewModel.ftInitCode.observe(activity, Observer { ftInitCode: Int? -> if (ftInitCode != ErrorInfo.MOK) { val error: String? = context.getString( R.string.specific_engine_init_failed, "ftEngine", ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!) ) Timber.tag(TAG).e("ftInitCode observe = $error") ToastUtils.showToast(error) } }) recognizeViewModel.frInitCode.observe(activity, Observer { frInitCode: Int? -> if (frInitCode != ErrorInfo.MOK) { val error: String? = context.getString( R.string.specific_engine_init_failed, "frEngine", frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!) ) Timber.tag(TAG).e("frInitCode observe = $error") ToastUtils.showToast(error) } }) recognizeViewModel.flInitCode.observe(activity, Observer { flInitCode: Int? -> if (flInitCode != ErrorInfo.MOK) { val error: String? = context.getString( R.string.specific_engine_init_failed, "flEngine", flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!) ) Timber.tag(TAG).e("flInitCode observe = $error") ToastUtils.showToast(error) } }) recognizeViewModel.recognizeConfiguration .observe(activity, Observer { recognizeConfiguration: RecognizeConfiguration? -> Timber.tag(TAG) .i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}") }) recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? -> Timber.tag(TAG).i("recognizeNotice observe notice = $notice") }) recognizeViewModel.recognizeUserId.observe( activity, Observer { compareResult: CompareResult -> if (currentStep != 2 || System.currentTimeMillis() - recognizeTime <= 2000) { //忽略非识别中及5秒内的再次识别 return@Observer } // if (isGoRecognize.not()) { // return@Observer // } recognizeTime = System.currentTimeMillis() activity.runOnUiThread { activity.showWaitingDialog("加载中,请稍后……") } Timber.tag(TAG) .i("recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}, step = $currentStep") recognitionTime = System.currentTimeMillis() recognitionWeight = lastWeight lastFaceTrackId = compareResult.trackId val faceEntity = compareResult.faceEntity val userId = faceEntity.userName currentUserId = userId isMember = faceEntity.userType == "1" isAgainLoadData = false if (userId == null) return@Observer //ToastUtils.showToast("人脸识别成功,userId = $userId") if (currentFood == null) { currentFood = activity.checkedItem } if (currentFood == null) { activity.runOnUiThread { activity.hideWaitingDialog() ToastUtils.showToast("请选择菜品") return@runOnUiThread } } // pauseCamera() step3ShowRecognizeResult(userId) }) recognizeViewModel.drawRectInfoText.observe(activity, Observer { info -> Timber.tag(TAG).i("drawRectInfoText observe info = $info") }) } private fun initArcView() { //在布局结束后才做初始化操作 binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this) recognizeViewModel.getCompareResultList().getValue() } /** * 调整View的宽高,使预览显示正常且采集框固定为 * * @param rgbPreview RGB预览View * @param previewView 显示预览数据的view * @param faceRectView 画框的view * @param previewSize 预览大小 * @param displayOrientation 相机旋转角度 * @param scale 缩放比例 * @return 调整后的LayoutParams */ private fun adjustPreviewViewSize( rgbPreview: View, previewView: View, faceRectView: FaceRectView, previewSize: Camera.Size, displayOrientation: Int, scale: Float ): ViewGroup.LayoutParams { val w = 600.dp val h = 1068.dp val layoutParams = FrameLayout.LayoutParams(w, h); previewView.setLayoutParams(layoutParams) faceRectView.setLayoutParams(layoutParams) return layoutParams } private fun initRgbCamera() { val cameraListener: CameraListener = object : CameraListener { override fun onCameraOpened( camera: Camera, cameraId: Int, displayOrientation: Int, isMirror: Boolean ) { Timber.tag(TAG) .d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation") activity.runOnUiThread({ val previewSizeRgb = camera.getParameters().getPreviewSize() val layoutParams = adjustPreviewViewSize( binding.dualCameraTexturePreviewRgb, binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView, previewSizeRgb, displayOrientation, 0.5F //0.6F ) Timber.tag(TAG) .d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}") Timber.tag(TAG) .d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}") Timber.tag(TAG).d( "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${ ConfigUtil.isDrawRgbRectHorizontalMirror( context ) }, isDrawRgbRectVerticalMirror = ${ ConfigUtil.isDrawRgbRectVerticalMirror( context ) }" ) // 调整识别窗口位置 rgbFaceRectTransformer = FaceRectTransformer( previewSizeRgb.width, previewSizeRgb.height, layoutParams.width, layoutParams.height, 90, cameraId, isMirror, true, true ) recognizeViewModel.onRgbCameraOpened(camera) recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer) }) } override fun onPreview(nv21: ByteArray?, camera: Camera?) { if (!isRecognition) { return } binding.dualCameraFaceRectView.clearFaceInfo() facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true) if (facePreviewInfoList != null && rgbFaceRectTransformer != null) { drawPreviewInfo(facePreviewInfoList!!) } if (facePreviewInfoList.isNullOrEmpty()) { currentUserId = null isMember = false } //recognizeViewModel.clearLeftFace(facePreviewInfoList) clearLeftFaceData() } override fun onCameraClosed() { Timber.tag(TAG).i("initRgbCamera onCameraClosed: ") } override fun onCameraError(e: java.lang.Exception) { Timber.tag(TAG).i("initRgbCamera onCameraError: %s", e.message) e.printStackTrace() } override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) { Timber.tag(TAG) .i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}") if (rgbFaceRectTransformer != null) { rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation } Timber.tag(TAG) .i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation") } } val measuredWidth = binding.dualCameraTexturePreviewRgb.measuredWidth val measuredHeight = binding.dualCameraTexturePreviewRgb.measuredHeight Timber.tag(TAG) .i("initRgbCamera measuredWidth=$measuredWidth,measuredHeight=$measuredHeight") val previewConfig: PreviewConfig = recognizeViewModel.previewConfig rgbCameraHelper = DualCameraHelper.Builder() .previewViewSize(Point(measuredWidth, measuredHeight)) // .previewViewSize(Point(608.dp, 456.dp)) // .previewViewSize(Point(1024, 768)) .rotation(activity.windowManager.defaultDisplay.rotation) .additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度 .previewSize(recognizeViewModel.loadPreviewSize()) // .previewSize(Point(1080, 720)) .specificCameraId(previewConfig.rgbCameraId) .isMirror(true) .previewOn(binding.dualCameraTexturePreviewRgb) .cameraListener(cameraListener) .build() rgbCameraHelper!!.init() // rgbCameraHelper!!.start() } /** * 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用 */ private fun initIrCamera() { Timber.tag(TAG).d("initIrCamera: livenessType = $livenessType") if (livenessType === LivenessType.RGB) { return } val irCameraListener: CameraListener = object : CameraListener { override fun onCameraOpened( camera: Camera, cameraId: Int, displayOrientation: Int, isMirror: Boolean ) { Timber.tag(TAG) .d("initIrCamera IR onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation") val previewSizeIr = camera.getParameters().getPreviewSize() val layoutParams = adjustPreviewViewSize( binding.dualCameraTexturePreviewRgb, binding.dualCameraTexturePreviewIr, binding.dualCameraFaceRectViewIr, previewSizeIr, displayOrientation, 0.25f ) irFaceRectTransformer = FaceRectTransformer( previewSizeIr.width, previewSizeIr.height, // layoutParams.width, layoutParams.height, layoutParams.width, layoutParams.height, displayOrientation, cameraId, isMirror, ConfigUtil.isDrawIrRectHorizontalMirror(context), ConfigUtil.isDrawIrRectVerticalMirror(context) ) recognizeViewModel.onIrCameraOpened(camera) recognizeViewModel.setIrFaceRectTransformer(irFaceRectTransformer) } override fun onPreview(nv21: ByteArray?, camera: Camera?) { recognizeViewModel.refreshIrPreviewData(nv21) } override fun onCameraClosed() { Timber.tag(TAG).i("initIrCamera onCameraClosed: ") } override fun onCameraError(e: java.lang.Exception) { Timber.tag(TAG).i("initIrCamera onCameraError: ${e.message}") e.printStackTrace() } override fun onCameraConfigurationChanged( cameraID: Int, displayOrientation: Int ) { if (irFaceRectTransformer != null) { irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation } Timber.tag(TAG) .i("initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation") } } val previewConfig = recognizeViewModel.previewConfig irCameraHelper = DualCameraHelper.Builder() .previewViewSize( Point( binding.dualCameraTexturePreviewIr.measuredWidth, binding.dualCameraTexturePreviewIr.measuredHeight ) ) // .previewViewSize(Point(133,100)) .rotation(activity.windowManager.defaultDisplay.rotation) .specificCameraId(previewConfig.irCameraId) .previewOn(binding.dualCameraTexturePreviewIr) .cameraListener(irCameraListener) .isMirror(true) .previewSize(recognizeViewModel.loadPreviewSize()) //相机预览大小设置,RGB与IR需使用相同大小 .additionalRotation(previewConfig.irAdditionalDisplayOrientation) //额外旋转角度 .build() irCameraHelper!!.init() try { irCameraHelper!!.start() } catch (e: RuntimeException) { ToastUtils.showToast(e.message + context.getString(R.string.camera_error_notice)) } } /** * 绘制RGB、IR画面的实时人脸信息 * * @param facePreviewInfoList RGB画面的实时人脸信息 */ private fun drawPreviewInfo(facePreviewInfoList: MutableList) { // Timber.tag(TAG).d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}") if (rgbFaceRectTransformer != null) { val rgbDrawInfoList: MutableList? = recognizeViewModel.getDrawInfo( facePreviewInfoList, LivenessType.RGB, openRectInfoDraw ) // 识别成功 binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList) } //binding.dualCameraFaceRectView.clearFaceInfo() //clearLeftFaceData() val listIsEmpty = facePreviewInfoList.isEmpty() val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null // Timber.tag(TAG) // .d("drawPreviewInfo,listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId") if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) { if (lastFaceTrackId != -1 && currentUserId != null) { //0-即放即取,1-余量计量 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 Timber.tag(TAG).i("drawPreviewInfo,$lastFaceTrackId 用户离开") lastFaceTrackId = -1 Timber.tag(TAG) .d("drawPreviewInfo,postUserData userNutritionData是否null: ${userNutrition == null}, currentFood是否null: ${currentFood == null},currentUserId是否null:${currentUserId == null}") if (userNutrition == null || currentFood == null || currentUserId == null) { return } Timber.tag(TAG).d("drawPreviewInfo,开始提交订单") createOrder(notEnoughOneBlock = { Timber.tag(TAG).d("drawPreviewInfo,notEnoughOneBlock") if (activity.settlementMode == 0) { Timber.tag(TAG).d("drawPreviewInfo,notEnoughOneBlock,联合模式") setTopInfoVisible() } // if (mealPickupMode == 0) { Timber.tag(TAG).d("drawPreviewInfo,notEnoughOneBlock,step2FaceRecognizing") currentStep = 2 step2FaceRecognizing(currentFood!!) // } }, successBlock = { Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成") // TODO: 测试人脸识别问题-------------------- //recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY) if (BuildConfig.DEBUG) { ToastUtils.showToast("订单已生成") } currentUserId = null isMember = false //ToastUtils.showToast("isKeepFaceState=$isKeepFaceState,mealPickupMode=$mealPickupMode") //|| isKeepFaceState // if (activity.settlementMode == 0) { // activity.isAnalyzing = false // step1FoodRecognizing() // } else { Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,mealPickupMode=$mealPickupMode") if (mealPickupMode == 1) { Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,step2FaceRecognizing") step2FaceRecognizing(currentFood!!) isKeepFaceState = false if (activity.settlementMode == 0) { Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,step2FaceRecognizing,联合支付") setTopInfoVisible() } } else { Timber.tag(TAG).d("drawPreviewInfo,successBlock,订单已生成,step1FoodRecognizing") Timber.tag(TAG).e("切回step1,------2000000000002") activity.isAnalyzing = false step1FoodRecognizing() } // } }) } } } /** * 人脸离开后调用创建订单接口 */ fun createOrder(notEnoughOneBlock: () -> Unit = {}, successBlock: () -> Unit) { //0-即放即取,1-余量计量 mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0 //if (settlementMode == 0) { // //使用联合支付方式 // ToastUtils.showToast("使用联合支付方式") // return //} val eatWeight = if (mealPickupMode == 0) { //即放即取 recognitionWeight } else { recognitionWeight - lastWeight } //val userNutritionParam = UserNutritionParam( // userId = userNutritionData?.userId!!, // foodId = currentFood?.foodId!!, // faceTime = recognitionTime, // faceEndTime = System.currentTimeMillis(), // eatWeight = eatWeight,//lastWeight // foodWeight = lastWeight //) //userViewModel.postUserNutritionData(listOf(userNutritionParam)) val realWeight = eatWeight.toDouble() val specWeight = currentFood!!.specWeight val eatNum = activity.getEatNum(realWeight, specWeight) activity.log("createOrder,realWeight=$realWeight,specWeight=$specWeight") activity.createOrder( foodInfo = currentFood!!, foodWeight = lastWeight, eatWeight = eatWeight, eatNum = eatNum, userId = currentUserId, isMember = isMember, notEnoughOneBlock = notEnoughOneBlock, successBlock = { activity.runOnUiThread { successBlock() } } ) } public fun clearLeftFaceData() { facePreviewInfoList?.let { recognizeViewModel.clearLeftFace(facePreviewInfoList!!) } facePreviewInfoList = null } fun hideNutritionView(chargeMode: Int) { binding.nutritionInclude.root.gone() binding.calorieInclude.root.gone() binding.tvFoodRecPrompt.visible() binding.ivRecImage.gone() binding.llPriceInfo.gone() binding.ivPreviewImage.visible() binding.ivFrame.visible() // if (chargeMode == 1) { // //不计费 //// binding.flFace.visible() // binding.ivRecImage.gone() // binding.llPriceInfo.gone() // binding.ivPreviewImage.gone() // binding.ivFrame.gone() // } else { // //计费 // binding.flFace.gone() // binding.llPriceInfo.gone() //// val imageUrl = if (currentFood!!.foodImg.isNullOrBlank()) currentFood!!.photoUri else currentFood!!.foodImg //// binding.ivRecImage.let { //// it.visible() //// it.load(imageUrl) //// } // binding.ivRecImage.gone() // binding.ivPreviewImage.visible() // binding.ivFrame.visible() // } } private var foodOrderList = mutableListOf() private val foodOrderAdapter by lazy { FoodOrderAdapter(foodOrderList) } /** * 加载订单列表 */ @SuppressLint("NotifyDataSetChanged", "SetTextI18n") private fun loadOrderList(userId: String, callback: () -> Unit) { binding.llTopInfo.gone() binding.flFoodDetail.visible() binding.detailInclude.let { it.root.updateLayoutParams { height = activity.screenSize[1] / 2 + 120.dp } it.rvFoodList.run { if (adapter == null) { layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false) adapter = foodOrderAdapter } } userViewModel.getFoodOrderList(userId) { model -> activity.runOnUiThread { if (model == null) return@runOnUiThread foodOrderList.clear() foodOrderList.addAll(model.list ?: emptyList()) //foodOrderAdapter.submitList(foodOrderList) foodOrderAdapter.notifyDataSetChanged() //binding.detailInclude.tvTotalWeight.text = "总重量:${model.eatWeightSum}克" Log.d(TAG, "addFoodToOrder: 更新总重量,订单列表------------------------------") // binding.detailInclude.tvTotalCalorie.text = "总热量:${model.calorie.roundedDecimalPlace(1)}千卡" jointPaymentQueryFinish = true //重新加载营养信息 callback() } } } } /** * 构造符合分辨率的 NV21 格式全黑帧 * @param width 图像宽度(如 1920) * @param height 图像高度(如 1080) * @return 合法的全黑帧字节数组 */ private fun createBlackNV21Frame(width: Int, height: Int): ByteArray { val frameSize = width * height val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5 // Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值) // 1. 填充 Y 分量(前 frameSize 个字节) // Arrays.fill(nv21Data, 0, frameSize, 0.toByte()) nv21Data.fill(0, 0, frameSize) // 2. 填充 UV 分量(后 frameSize/2 个字节) // Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte()) nv21Data.fill(128.toByte(), frameSize, nv21Data.size) return nv21Data } // 调用示例:构造 1920×1080 的全黑帧(长度 1382400) // private val emptyFrame = createBlackNV21Frame(1920, 1080) private val emptyFrame = createBlackNV21Frame(1280, 720) }