实现了双屏摄像头显示,添加了部分代码

This commit is contained in:
zxj
2025-07-31 11:40:56 +08:00
parent 56e0a85a4c
commit de4c5f1c75
86 changed files with 3446 additions and 809 deletions
+8
View File
@@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-07-31T00:35:57.879961400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=2FD2511004054749" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
+2
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
@@ -10,6 +11,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/lib_face" />
</set>
</option>
</GradleProjectSettings>
Generated
+3 -1
View File
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+19 -1
View File
@@ -7,6 +7,14 @@ plugins {
}
android {
signingConfigs {
getByName("debug") {
storeFile = file("./swkey.jks")
storePassword = "123456"
keyAlias = "android"
keyPassword = "123456"
}
}
namespace = "com.sw.dualscreen"
compileSdk = 35
@@ -18,6 +26,9 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
}
}
buildTypes {
@@ -42,7 +53,14 @@ android {
}
dependencies {
// implementation(
// fileTree(
// mapOf(
// "dir" to "libs",
// "include" to listOf("*.aar", "*.jar")
// )
// )
// )
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
+6 -11
View File
@@ -16,7 +16,7 @@
android:maxSdkVersion="32" />
<application
android:name=".App"
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@@ -28,16 +28,7 @@
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".activity.Camera2Activity"
android:exported="true">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
</activity>
<activity
android:name=".activity.MainActivity"
android:name=".activity.InitActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -45,6 +36,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activity.MainActivity"
android:exported="true">
</activity>
</application>
</manifest>
@@ -0,0 +1,58 @@
package com.sw.dualscreen
object GlobalData {
/**
* 同一设备全局使用的设备编号
*/
// var globalEquipmentCode: String = "202501171634"
var restId: String = ""
/**
* app版本号
*/
var appVersion: String = "1"
/**
* 具体业务baseurl
*/
var appBaseUrl: String = ""
/**
* 横排数量
*/
var arrayCross: Int = 2
/**
* 竖排数量
*/
var arrayVertical: Int = 11
/**
* 排列方式 0 垂直 1 水平
*/
var arrayMode: Int = 0
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "085F-118G-Q3AB-1WVJ"
}
/**
* 全局常量
*/
object Constants {
/**
* 自动关闭时间
*/
const val AUTO_CLOSE_TIME: Long = 3
}
/**
* key
*/
object GlobalKey {
const val KEY_EQUIPMENT_INFO = "equipmentInfo"
const val KEY_TOKEN = "tokenKey"
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
}
@@ -1,9 +1,13 @@
package com.sw.dualscreen
import android.app.Application
import com.sw.plate.App
import timber.log.Timber
class App : Application() {
class MyApp : App() {
companion object {
const val DEBUG: Boolean = true
}
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
@@ -1,156 +0,0 @@
package com.sw.dualscreen.activity
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.os.Bundle
import android.util.Log
import android.view.Surface
import android.view.TextureView
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.sw.dualscreen.R
class Camera2Activity : AppCompatActivity() {
private lateinit var cameraManager: CameraManager
private var cameraIdList: Array<String> = arrayOf()
private var currentCameraIdIndex = 0
private lateinit var textureView: TextureView
private var cameraDevice: CameraDevice? = null
private var cameraCaptureSession: CameraCaptureSession? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera2)
textureView = findViewById(R.id.textureView)
cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
// 获取摄像头列表
cameraIdList = cameraManager.cameraIdList
if (cameraIdList.isEmpty()) {
Toast.makeText(this, "没有可用的摄像头", Toast.LENGTH_SHORT).show()
finish()
}
// 设置TextureView监听
textureView.surfaceTextureListener = surfaceTextureListener
// 切换摄像头按钮
findViewById<Button>(R.id.switchButton).setOnClickListener {
switchCamera()
}
}
private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
openCamera(cameraIdList[currentCameraIdIndex])
}
override fun onSurfaceTextureSizeChanged(
surface: SurfaceTexture,
width: Int,
height: Int
) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = true
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
}
private fun openCamera(cameraId: String) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED
) {
return
}
try {
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
cameraDevice = device
createCameraPreviewSession()
}
override fun onDisconnected(device: CameraDevice) {
device.close()
cameraDevice = null
}
override fun onError(device: CameraDevice, error: Int) {
device.close()
cameraDevice = null
}
}, null)
} catch (e: Exception) {
Log.e("Camera2", "打开摄像头失败", e)
}
}
private fun createCameraPreviewSession() {
val surfaceTexture = textureView.surfaceTexture
surfaceTexture?.setDefaultBufferSize(textureView.width, textureView.height)
val surface = Surface(surfaceTexture)
try {
val previewRequestBuilder = cameraDevice?.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
)?.apply {
addTarget(surface)
}
cameraDevice?.createCaptureSession(
listOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
cameraCaptureSession = session
previewRequestBuilder?.let { builder ->
builder.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
)
session.setRepeatingRequest(builder.build(), null, null)
}
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Toast.makeText(this@Camera2Activity, "配置失败", Toast.LENGTH_SHORT).show()
}
},
null
)
} catch (e: Exception) {
Log.e("Camera2", "创建预览会话失败", e)
}
}
private fun switchCamera() {
if (cameraIdList.size <= 1) {
Toast.makeText(this, "只有一个摄像头", Toast.LENGTH_SHORT).show()
return
}
// 关闭当前摄像头
cameraCaptureSession?.close()
cameraCaptureSession = null
cameraDevice?.close()
cameraDevice = null
// 切换到下一个摄像头
currentCameraIdIndex = (currentCameraIdIndex + 1) % cameraIdList.size
openCamera(cameraIdList[currentCameraIdIndex])
}
override fun onDestroy() {
super.onDestroy()
cameraCaptureSession?.close()
cameraDevice?.close()
}
}
@@ -0,0 +1,50 @@
package com.sw.dualscreen.activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.databinding.ActivityInitBinding
import com.sw.dualscreen.viewmodel.DeviceViewModel
import com.sw.plate.utils.AppUtil
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
class InitActivity : AppCompatActivity() {
private lateinit var binding: ActivityInitBinding
private val viewModel by viewModels<DeviceViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityInitBinding.inflate(layoutInflater)
setContentView(binding.root)
val deviceId = AppUtil.getUDID(this)
val isSuccess = viewModel.checkEquipmentInfo()
if (isSuccess) {
goMainActivity()
}
binding.tvDeviceId.text = deviceId
binding.btConfirm.setOnClickListener {
viewModel.getDeviceToken()
}
registerDataChange()
}
private fun registerDataChange() {
lifecycleScope.launch {
viewModel.deviceInfoResult.drop(1).collect {
if (it != true) return@collect
goMainActivity()
}
}
}
private fun goMainActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}
@@ -6,10 +6,14 @@ import android.hardware.display.DisplayManager
import android.os.Bundle
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.AdapterView
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
@@ -17,14 +21,23 @@ import com.google.common.util.concurrent.ListenableFuture
import com.sw.dualscreen.databinding.ActivityMainBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.presentation.SecondaryScreenPresentation
import com.sw.dualscreen.viewmodel.MyViewModel
import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import timber.log.Timber
import java.io.File
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private val viewModel = MyViewModel()
private val viewModel by viewModels<UserViewModel>()
private lateinit var imageCapture: ImageCapture
private var isAnalyzing = true // 控制是否进行图像分析
private lateinit var presentation: SecondaryScreenPresentation
private val executor = Executors.newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -32,51 +45,136 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.main)
Timber.d("UDID = ${AppUtil.getUDID(this)}")
initView()
startCamera()
setupSecondaryDisplay()
setupCamera()
initData()
}
private fun initData() {
viewModel.getEquipmentToken()
}
private fun initView() {
viewModel.activeEngine()
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
outline.setRoundRect(0, 0, view.width, view.height, 280f.dp)
outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
}
}
binding.previewView.clipToOutline = true
binding.tvToSearch.setOnClickListener {
val bottomSheetDialog = CustomBottomSheetDialog.newInstance()
bottomSheetDialog.show(supportFragmentManager, "CustomBottomSheetDialog")
}
binding.spinner.setItems(arrayOf("即放即取", "持续取餐"))
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
private fun startCamera() {
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
private fun setupCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 创建预览用例
// 1. 创建预览用例
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
// 选择后置摄像头
// 2. 创建图像分析用例(用于副屏显示)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担
.build()
.also { analysis ->
analysis.setAnalyzer(executor) { imageProxy ->
if (isAnalyzing) {
val bitmap = imageProxy.toBitmap()
runOnUiThread {
presentation.updateImage(bitmap) // 更新副屏
}
}
imageProxy.close() // 必须关闭以释放资源
}
}
// 3. 创建拍照用例
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// .setTargetResolution(Size(1920, 1080)) // 设置拍照分辨率
.build()
// 4. 选择摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// 解绑所有用例
cameraProvider.unbindAll()
// 绑定到生命周期
// 绑定三个用例到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview
this, cameraSelector, preview, imageAnalysis, imageCapture
)
} catch (exc: Exception) {
Timber.e(exc, "Use case binding failed")
Timber.e(exc.message)
}
}, ContextCompat.getMainExecutor(this))
}
// 拍照功能
private fun takePhoto() {
// 临时停止分析以避免干扰
isAnalyzing = false
// 创建输出选项
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(
File(
cacheDir.path, "IMG_${System.currentTimeMillis()}.jpg"
)
).build()
// 执行拍照
imageCapture.takePicture(
outputFileOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
runOnUiThread {
ToastUtils.showToast("照片保存成功: ${outputFileResults.savedUri}")
}
// 恢复图像分析
isAnalyzing = true
}
override fun onError(exception: ImageCaptureException) {
Timber.e("拍照失败: ${exception.message}")
runOnUiThread {
ToastUtils.showToast("拍照失败: ${exception.message}")
}
// 恢复图像分析
isAnalyzing = true
}
}
)
}
private fun setupSecondaryDisplay() {
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displays = displayManager.displays
@@ -85,7 +183,7 @@ class MainActivity : AppCompatActivity() {
if (displays.size > 1) {
val secondaryDisplay = displays[1]
// 创建副屏的Presentation
val presentation =
presentation =
SecondaryScreenPresentation(
activity = this,
this,
@@ -1,9 +0,0 @@
package com.sw.dualscreen.adapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
abstract class BaseViewHolder<B : ViewBinding, T>(val binding: B) :
RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: T)
}
@@ -0,0 +1,38 @@
package com.sw.dualscreen.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
/**
* 通用子项适配器(支持ViewBinding
*/
class GenericItemAdapter<T, VB : ViewBinding>(
private var items: List<T>,
private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val bindCallback: VB.(item: T, position: Int) -> Unit,
) : RecyclerView.Adapter<GenericItemAdapter<T, VB>.ViewHolder>() {
inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: T, position: Int) {
binding.bindCallback(item, position)
}
}
fun updateData(newItem: List<T>) {
this.items = newItem
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = bindingInflater(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount(): Int = items.size
}
@@ -1,31 +0,0 @@
package com.sw.dualscreen.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
class GridRecyclerAdapter<T, B : ViewBinding>(
private val itemList: List<T>,
private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> B,
private val bindHolder: (B, T) -> Unit,
private val onItemClick: ((T) -> Unit)? = null
) : RecyclerView.Adapter<BaseViewHolder<B, T>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<B, T> {
val inflater = LayoutInflater.from(parent.context)
val binding = bindingInflater(inflater, parent, false)
return object : BaseViewHolder<B, T>(binding) {
override fun bind(item: T) = bindHolder(binding, item)
}
}
override fun onBindViewHolder(holder: BaseViewHolder<B, T>, position: Int) {
holder.bind(itemList[position])
holder.itemView.setOnClickListener {
onItemClick?.invoke(itemList[position])
}
}
override fun getItemCount(): Int = itemList.size
}
@@ -1,16 +1,17 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
/**
* item间距
* 为RecyclerView添加间距的ItemDecoration
*/
class GridSpacingItemDecoration(
private val spanCount: Int,
private val spacing: Int,
private val includeEdge: Boolean
private val spanCount: Int, // 列数
private val spacing: Int, // 间距(dp)
private val includeEdge: Boolean // 是否包含边缘
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
@@ -19,18 +20,35 @@ class GridSpacingItemDecoration(
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
val column = position % spanCount
val position = parent.getChildAdapterPosition(view) // item position
val column = position % spanCount // item column
if (includeEdge) {
// 左右间距计算
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
if (position < spanCount) outRect.top = spacing
outRect.bottom = spacing
// 第一行添加顶部间距
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing // 每行底部都添加间距
} else {
// 左右间距计算(不包含边缘)
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
if (position >= spanCount) outRect.top = spacing
// 不是第一行时添加顶部间距
if (position >= spanCount) {
outRect.top = spacing
}
}
}
}
/**
* 将dp值转换为px
*/
fun Context.dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
@@ -1,8 +1,15 @@
package com.sw.dualscreen.ext
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import android.graphics.Rect
import android.graphics.YuvImage
import android.util.TypedValue
import androidx.annotation.Dimension
import androidx.camera.core.ImageProxy
import java.io.ByteArrayOutputStream
/**
* 将 Int 值转换为 dp 值
@@ -46,4 +53,30 @@ val Float.sp: Float
TypedValue.COMPLEX_UNIT_SP,
this,
Resources.getSystem().displayMetrics
)
)
// 添加扩展函数
fun ImageProxy.toBitmap(): Bitmap {
val yBuffer = planes[0].buffer // Y
val uBuffer = planes[1].buffer // U
val vBuffer = planes[2].buffer // V
val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()
val nv21 = ByteArray(ySize + uSize + vSize)
// Y
yBuffer.get(nv21, 0, ySize)
// U/V
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, this.width, this.height), 100, out)
val imageBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
@@ -0,0 +1,18 @@
package com.sw.dualscreen.model.request
import android.os.Parcelable
import com.sw.dualscreen.GlobalData
import kotlinx.parcelize.Parcelize
@Parcelize
data class UserNutritionParam(
val appVersion: String = GlobalData.appVersion,
val restId: String = GlobalData.restId,
val userId: String,
val foodId: String,
val faceTime: Long,
val faceEndTime: Long,
val eatWeight: Double,
val foodWeight: Double,
val deviceId: String = ""
) : Parcelable
@@ -0,0 +1,17 @@
package com.sw.dualscreen.model.response
data class ApiResponse<T>(
val code: Int? = 0,
val message: String? = "",
val result: T? = null,
val success: Boolean? = false,
val timestamp: Long? = 0
) {
fun isSuccess(): Boolean {
return success == true
}
}
@@ -0,0 +1,72 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class EquipmentInfo(
@SerializedName("appPackageLocalUrl")
val appPackageLocalUrl: String? = "",
@SerializedName("appPackageUrl")
val appPackageUrl: String? = "",
@SerializedName("arcsoftActiveKey")
val arcsoftActiveKey: String? = "",
@SerializedName("arcsoftAppId")
val arcsoftAppId: String? = "",
@SerializedName("arcsoftSdkKey")
val arcsoftSdkKey: String? = "",
@SerializedName("arrayCross")
val arrayCross: Int? = 0,
@SerializedName("arrayMode")
val arrayMode: String? = "",
@SerializedName("arrayVertical")
val arrayVertical: Int? = 0,
@SerializedName("canteenId")
val canteenId: String? = "",
@SerializedName("canteenName")
val canteenName: String? = "",
@SerializedName("clientServerIp")
val clientServerIp: String? = "",
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("customerName")
val customerName: String? = "",
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
@SerializedName("equipmentName")
val equipmentName: String? = "",
@SerializedName("equipmentName_dictText")
val equipmentNameDictText: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: Int? = 0,
@SerializedName("mqName")
val mqName: String? = "",
@SerializedName("mqPassword")
val mqPassword: String? = "",
@SerializedName("orgCode")
val orgCode: String? = "",
@SerializedName("owningTrack")
val owningTrack: Int? = 0,
@SerializedName("owningTrackOrder")
val owningTrackOrder: Int? = 0,
@SerializedName("screenHtml")
val screenHtml: String? = "",
@SerializedName("serviceRequestAddress")
val serviceRequestAddress: String? = "",
@SerializedName("status")
val status: Int? = 0,
@SerializedName("sysOrgCode")
val sysOrgCode: String? = "",
@SerializedName("updateBy")
val updateBy: String? = "",
@SerializedName("updateTime")
val updateTime: String? = "",
@SerializedName("zhstServerIp")
val zhstServerIp: String? = ""
) : Parcelable
@@ -0,0 +1,274 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 搜索的食物信息
*/
@Parcelize
data class FoodInfo(
@SerializedName("foodLabel")
val foodLabel: String? = "",
@SerializedName("foodName")
val foodName: String? = "",
@SerializedName("foodTypeAndRealIntakeVoList")
val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo?>? = listOf(),
/**
* 食物id
*/
@SerializedName("id")
val id: String? = "",
@SerializedName("imgUrl")
val imgUrl: String? = "",
@SerializedName("stFoodInfoMaterial")
val stFoodInfoMaterial: StFoodInfoMaterial? = StFoodInfoMaterial(),
@SerializedName("stFoodInfoPagoda")
val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
@SerializedName("stFoodInfoPagodaAPPVO")
val stFoodInfoPagodaAPPVO: StFoodInfoPagodaAPPVO? = StFoodInfoPagodaAPPVO(),
@SerializedName("stFoodInfoSetting")
val stFoodInfoSetting: StFoodInfoSetting? = StFoodInfoSetting(),
@SerializedName("stFoodInfoSpecificationList")
val stFoodInfoSpecificationList: List<StFoodInfoSpecification?>? = listOf()
) : Parcelable {
@Parcelize
data class FoodTypeAndRealIntakeVo(
@SerializedName("childMaterClassName")
val childMaterClassName: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("goodsId")
val goodsId: String? = "",
@SerializedName("goodsName")
val goodsName: String? = "",
@SerializedName("materClassName")
val materClassName: String? = "",
@SerializedName("materId")
val materId: String? = "",
@SerializedName("materialType")
val materialType: String? = "",
@SerializedName("realityIntake")
val realityIntake: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoMaterial(
@SerializedName("ash")
val ash: Double? = 0.0,
@SerializedName("avitE")
val avitE: Double? = 0.0,
@SerializedName("ca")
val ca: Double? = 0.0,
@SerializedName("cho")
val cho: Double? = 0.0,
@SerializedName("cholesterol")
val cholesterol: Double? = 0.0,
@SerializedName("cu")
val cu: Double? = 0.0,
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("dietFiber")
val dietFiber: Double? = 0.0,
@SerializedName("elementI")
val elementI: Double? = 0.0,
@SerializedName("elementK")
val elementK: String? = "",
@SerializedName("elementP")
val elementP: String? = "",
@SerializedName("energyKcal")
val energyKcal: Double? = 0.0,
@SerializedName("energyKj")
val energyKj: Double? = 0.0,
@SerializedName("fat")
val fat: Double? = 0.0,
@SerializedName("fe")
val fe: Double? = 0.0,
@SerializedName("folate")
val folate: Double? = 0.0,
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("historyStatus")
val historyStatus: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("mg")
val mg: Double? = 0.0,
@SerializedName("mn")
val mn: Double? = 0.0,
@SerializedName("na")
val na: Double? = 0.0,
@SerializedName("naicin")
val naicin: Double? = 0.0,
@SerializedName("protein")
val protein: Double? = 0.0,
@SerializedName("retionl")
val retionl: Double? = 0.0,
@SerializedName("riboflavin")
val riboflavin: Double? = 0.0,
@SerializedName("se")
val se: Double? = 0.0,
@SerializedName("thiamin")
val thiamin: Double? = 0.0,
@SerializedName("totCarotene")
val totCarotene: Double? = 0.0,
@SerializedName("vitA")
val vitA: Double? = 0.0,
@SerializedName("vitB12")
val vitB12: Double? = 0.0,
@SerializedName("vitB6")
val vitB6: Double? = 0.0,
@SerializedName("vitC")
val vitC: Double? = 0.0,
@SerializedName("vitE")
val vitE: Double? = 0.0,
@SerializedName("water")
val water: Double? = 0.0,
@SerializedName("zn")
val zn: Double? = 0.0
) : Parcelable
@Parcelize
data class StFoodInfoPagoda(
@SerializedName("aquatic")
val aquatic: String? = "",
@SerializedName("birds")
val birds: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("egg")
val egg: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("livestock")
val livestock: String? = "",
@SerializedName("milk")
val milk: String? = "",
@SerializedName("nuts")
val nuts: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("potato")
val potato: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoPagodaAPPVO(
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("fruitsRecommend")
val fruitsRecommend: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("grainRecommend")
val grainRecommend: String? = "",
@SerializedName("meat")
val meat: String? = "",
@SerializedName("meatRecommend")
val meatRecommend: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("soyaRecommend")
val soyaRecommend: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = "",
@SerializedName("vegetableRecommend")
val vegetableRecommend: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoSetting(
@SerializedName("addFoodWeight")
val addFoodWeight: String? = "",
@SerializedName("bowlPlateWeight")
val bowlPlateWeight: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("foodStatus")
val foodStatus: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("inventoryStatus")
val inventoryStatus: Boolean? = false,
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("replenishWeight")
val replenishWeight: String? = "",
@SerializedName("residueType")
val residueType: String? = "",
@SerializedName("tablewareStatus")
val tablewareStatus: Boolean? = false,
@SerializedName("tablewareWeight")
val tablewareWeight: String? = "",
@SerializedName("warningStatus")
val warningStatus: String? = "",
@SerializedName("weighStatus")
val weighStatus: String? = "",
@SerializedName("weighUnitId")
val weighUnitId: String? = "",
@SerializedName("weighUnitName")
val weighUnitName: String? = ""
) : Parcelable
@Parcelize
data class StFoodInfoSpecification(
@SerializedName("bowlPlateWeight")
val bowlPlateWeight: Double? = 0.0,
@SerializedName("defaultStatus")
val defaultStatus: String? = "",
@SerializedName("delFlag")
val delFlag: String? = "",
@SerializedName("foodId")
val foodId: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: String? = "",
@SerializedName("isSyncCopy")
val isSyncCopy: String? = "",
@SerializedName("specId")
val specId: String? = "",
@SerializedName("specName")
val specName: String? = "",
@SerializedName("specPrice")
val specPrice: String? = "",
@SerializedName("specWeight")
val specWeight: Double? = 0.0
) : Parcelable
}
@@ -0,0 +1,77 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 就餐数据
*/
@Parcelize
data class NutritionData(
@SerializedName("cho")
val cho: String? = "",
@SerializedName("dietFiber")
val dietFiber: String? = "",
@SerializedName("energy")
val energy: String? = "",
@SerializedName("fat")
val fat: String? = "",
@SerializedName("foodTypeAndRealIntakeVoList")
val foodTypeAndRealIntakeVoList: List<String?>? = listOf(),
@SerializedName("foodWeight")
val foodWeight: String? = "",
@SerializedName("message")
val message: String? = "",
@SerializedName("na")
val na: String? = "",
@SerializedName("protein")
val protein: String? = "",
@SerializedName("recommendMax")
val recommendMax: String? = "",
@SerializedName("recommendMin")
val recommendMin: String? = "",
@SerializedName("stFoodInfoPagoda")
val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
@SerializedName("stUserFoodInfoList")
val stUserFoodInfoList: List<String?>? = listOf(),
@SerializedName("totalEnergyCalculateScore")
val totalEnergyCalculateScore: String? = "",
@SerializedName("userDept")
val userDept: String? = "",
@SerializedName("userId")
val userId: String? = "",
@SerializedName("userName")
val userName: String? = ""
) : Parcelable {
@Parcelize
data class StFoodInfoPagoda(
@SerializedName("fruits")
val fruits: String? = "",
@SerializedName("fruitsRecommend")
val fruitsRecommend: String? = "",
@SerializedName("grain")
val grain: String? = "",
@SerializedName("grainRecommend")
val grainRecommend: String? = "",
@SerializedName("meat")
val meat: String? = "",
@SerializedName("meatRecommend")
val meatRecommend: String? = "",
@SerializedName("oil")
val oil: String? = "",
@SerializedName("salt")
val salt: String? = "",
@SerializedName("soya")
val soya: String? = "",
@SerializedName("soyaRecommend")
val soyaRecommend: String? = "",
@SerializedName("sugar")
val sugar: String? = "",
@SerializedName("vegetable")
val vegetable: String? = "",
@SerializedName("vegetableRecommend")
val vegetableRecommend: String? = ""
) : Parcelable
}
@@ -0,0 +1,14 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class UserFaceInfo(
@SerializedName("data")
val `data`: List<UserFaceModel>? = listOf(),
@SerializedName("nextPageIndex")
val nextPageIndex: Int? = 0
) : Parcelable
@@ -0,0 +1,23 @@
package com.sw.dualscreen.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
@Parcelize
data class UserFaceModel(
@SerializedName("faceFeature")
val faceFeature: String? = "",
@SerializedName("faceFeatureString")
val faceFeatureString: String? = "",
@SerializedName("faceType")
val faceType: String? = "",
@SerializedName("userFaceId")
val userFaceId: String? = "",
@SerializedName("userId")
val userId: String? = ""
) : Parcelable
@@ -0,0 +1,44 @@
package com.sw.dualscreen.network
import com.sw.dualscreen.MyApp
import com.sw.dualscreen.network.api.ApiService
import com.sw.dualscreen.network.interceptor.RequestInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import java.util.concurrent.TimeUnit
object ApiClient {
private const val BASE_URL = "http://device.shuziweidao.com:8889/"
private const val TIME_OUT = 30L // 超时时间(秒)
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor(logger = {
Timber.d("okhttp logger ==>${it}")
}).apply {
level = if (MyApp.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.addInterceptor(RequestInterceptor())
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
.build()
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
@@ -0,0 +1,91 @@
package com.sw.dualscreen.network.api
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.NutritionData
import com.sw.dualscreen.model.response.UserFaceInfo
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.Url
interface ApiService {
/**
* device获取token
*/
@GET("sys/getEquipmentToken")
suspend fun getDeviceToken(
@Query("qrcodeId") qrcodeId: String,
@Query("appVersion") appVersion: String = GlobalData.appVersion
): ApiResponse<String>
/**
*获取配置信息
*/
@GET("equipment/stEquipment/queryByEquipmentCode")
suspend fun getDeviceInfo(
@Query("equipmentCode") equipmentCode: String,
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Header("X-Access-Token") token: String
): ApiResponse<EquipmentInfo>
/**
* 获取业务服务器token
*/
@GET
suspend fun getEquipmentToken(
@Url url: String = "${GlobalData.appBaseUrl}/sys/getEquipmentToken",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("qrcodeId") qrcodeId: String
): ApiResponse<String>
/**
* 获取人脸数据
*/
@GET
suspend fun getUserFaceCache(
@Url url: String = "${GlobalData.appBaseUrl}/stapi/cquser/getUserFaceCache/v2",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("pageIndex") pageIndex: Int
): ApiResponse<UserFaceInfo>
/**
* 获取档口菜品信息
*/
@GET
suspend fun getRestInfoFoodsByType(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("restId") restId: String,
@Query("type") type: Int,
@Query("foodName") foodName: String,
): ApiResponse<List<FoodInfo>>
/**
* 通过用户信息获取就餐数据
*/
@GET
suspend fun getUserNutritionData(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getUserNutritionData/face",
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Query("restId") restId: String,
@Query("userId") userId: String,
@Query("foodId") foodId: String,
): ApiResponse<NutritionData>
/**
* 提交就餐数据
*/
@POST
suspend fun postUserNutritionData(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/userEatFood/doubleBalance/face/stall",
@Body param: UserNutritionParam
): ApiResponse<String>
}
@@ -0,0 +1,36 @@
package com.sw.dualscreen.network.interceptor
import android.text.TextUtils
import com.sw.dualscreen.GlobalKey
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
/**
* 请求拦截器
*/
class RequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
.header("X-Access-Token", getToken(originalRequest))
val newRequest = requestBuilder.build()
return chain.proceed(newRequest)
}
private fun getToken(originRequest: Request): String {
val tokenParam = originRequest.header("X-Access-Token")
if (!TextUtils.isEmpty(tokenParam)) return tokenParam!!
// 从本地获取token的逻辑
val spUtil = SPUtil.getInstance(context = App.getContext())
return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
}
}
@@ -4,30 +4,30 @@ import android.Manifest
import android.app.Presentation
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Point
import android.hardware.Camera
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.Display
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.MainActivity
import com.sw.dualscreen.databinding.PresentationSecondaryScreenBinding
import com.sw.dualscreen.viewmodel.MyViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
@@ -50,15 +50,17 @@ class SecondaryScreenPresentation(
val activity: MainActivity,
context: Context,
display: Display,
viewModel: MyViewModel,
viewModel: UserViewModel,
private val onDismissListener: () -> Unit = {},
val recognizeViewModel: RecognizeViewModel
) : Presentation(context, display), ViewTreeObserver.OnGlobalLayoutListener {
private lateinit var binding: PresentationSecondaryScreenBinding
private var cameraDevice: CameraDevice? = null
private var cameraCaptureSession: CameraCaptureSession? = null
// private var cameraCaptureSession: CameraCaptureSession? = null
private lateinit var cameraManager: CameraManager
private var cameraIdList: Array<String> = arrayOf()
lateinit var previewView: PreviewView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -67,6 +69,8 @@ class SecondaryScreenPresentation(
setContentView(binding.root)
window?.setBackgroundDrawableResource(android.R.color.transparent)
previewView = binding.previewView
cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
// 获取摄像头列表
cameraIdList = cameraManager.cameraIdList
@@ -75,10 +79,15 @@ class SecondaryScreenPresentation(
return
}
cameraIdList.forEach {
Timber.d("摄像头id列表 = ${it}")
}
initArcViewModel()
initArcView()
openRectInfoDraw = true
openCamera(cameraIdList.last())
// binding.includeUserHeat.ivHeat.showText("1070千卡")
}
override fun onDisplayRemoved() {
@@ -86,19 +95,50 @@ class SecondaryScreenPresentation(
onDismissListener()
}
fun updateImage(bitmap: Bitmap) {
activity.runOnUiThread {
binding.ivImg.setImageBitmap(bitmap)
}
}
/**
* 用户1 显示相机预览
*/
fun step1(foodImg: Uri) {
}
/**
* 显示人脸识别
*/
fun step2() {
}
/**
* 显示识别出的菜品信息
*/
fun step3() {
}
private fun openCamera(cameraId: String) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED
) {
return
}
try {
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
cameraDevice = device
// createCameraPreviewSession()
recognizeViewModel.init()
recognizeViewModel.init(
PreviewConfig(
2, 4, 0, 0
)
)
initRgbCamera()
resumeCamera()
}
@@ -118,44 +158,6 @@ class SecondaryScreenPresentation(
}
}
private fun createCameraPreviewSession() {
val textureView = binding.dualCameraTexturePreviewRgb
val surfaceTexture = textureView.surfaceTexture
surfaceTexture?.setDefaultBufferSize(textureView.width, textureView.height)
val surface = Surface(surfaceTexture)
try {
val previewRequestBuilder = cameraDevice?.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
)?.apply {
addTarget(surface)
}
cameraDevice?.createCaptureSession(
listOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
cameraCaptureSession = session
previewRequestBuilder?.let { builder ->
builder.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
)
session.setRepeatingRequest(builder.build(), null, null)
}
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Toast.makeText(context, "配置失败", Toast.LENGTH_SHORT).show()
}
},
null
)
} catch (e: Exception) {
Log.e("Camera2", "创建预览会话失败", e)
}
}
// 虹软人脸配置 ⬇
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
@@ -296,7 +298,7 @@ class SecondaryScreenPresentation(
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 1f
previewSizeRgb, 90, 1f
)
Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
@@ -358,7 +360,7 @@ class SecondaryScreenPresentation(
)
)
.rotation(activity.windowManager.defaultDisplay.rotation)
.additionalRotation(0) // 角度
.additionalRotation(90) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
@@ -420,6 +422,7 @@ class SecondaryScreenPresentation(
}
fun resumeCamera() {
Timber.d("resumeCamera")
isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
rgbCameraHelper!!.start()
@@ -427,6 +430,7 @@ class SecondaryScreenPresentation(
}
fun pauseCamera() {
Timber.d("pauseCamera")
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
@@ -0,0 +1,50 @@
package com.sw.dualscreen.repository
import com.google.gson.JsonParseException
import com.sw.dualscreen.model.response.ApiResponse
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import javax.net.ssl.SSLHandshakeException
abstract class BaseRepository {
suspend fun <T> safeApiCall(apiCall: suspend () -> ApiResponse<T>): ApiResponse<T> {
return try {
apiCall()
} catch (e: Exception) {
Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
when (e) {
is HttpException -> {
ApiResponse(code = e.code(), message = e.message())
}
is SocketTimeoutException -> {
ApiResponse(code = -2, message = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = -3, message = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = -4, message = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = -5, message = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = -6, message = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = -1, message = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
}
}
@@ -0,0 +1,97 @@
package com.sw.dualscreen.repository
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.NutritionData
import com.sw.dualscreen.model.response.UserFaceInfo
import com.sw.dualscreen.network.api.ApiService
/**
* 远程数据处理
*/
class RemoteRepository constructor(
private val apiService: ApiService
) : BaseRepository() {
/**
* 生成token
*/
suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
return safeApiCall {
apiService.getDeviceToken(
qrcodeId
)
}
}
/**
* 获取设备信息
*/
suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
return safeApiCall {
apiService.getDeviceInfo(
equipmentCode,
token = token
)
}
}
/**
* 获取业务服务器token
*/
suspend fun getEquipmentToken(qrcodeId: String): ApiResponse<String> {
return safeApiCall {
apiService.getEquipmentToken(qrcodeId = qrcodeId)
}
}
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(
pageIndex: Int
): ApiResponse<UserFaceInfo> {
return safeApiCall {
apiService.getUserFaceCache(pageIndex = pageIndex)
}
}
/**
* 获取档口菜品信息
* @param restId 从device服务获取的canteenId字段
* @param type 0全部 1餐次
*/
suspend fun getRestInfoFoodsByType(
restId: String = GlobalData.restId,
type: Int = 1,
foodName: String,
): ApiResponse<List<FoodInfo>> {
return safeApiCall {
apiService.getRestInfoFoodsByType(restId = restId, type = type, foodName = foodName)
}
}
/**
* 通过用户信息获取就餐数据
*/
suspend fun getUserNutritionData(
restId: String = GlobalData.restId,
userId: String,
foodId: String,
): ApiResponse<NutritionData> {
return safeApiCall {
apiService.getUserNutritionData(restId = restId, userId = userId, foodId = foodId)
}
}
/**
* 提交就餐数据
*/
suspend fun postUserNutritionData(
param: UserNutritionParam
): ApiResponse<String> {
return safeApiCall { apiService.postUserNutritionData(param = param) }
}
}
@@ -0,0 +1,156 @@
package com.sw.dualscreen.utils
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import timber.log.Timber
import java.io.File
object FileUtils {
/**
* 从Uri获取File
* example: file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
*/
private fun getFileFromUri(context: Context, uri: Uri): File? {
Timber.d("getFileFromUri uri = ${uri.scheme}")
return when (uri.scheme) {
"file" -> File(uri.path ?: return null)
"content" -> {
try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val cacheDir = context.cacheDir
val file = File.createTempFile(
"upload_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
file.outputStream().use { output ->
inputStream.copyTo(output)
}
file
} catch (e: Exception) {
Timber.e(e)
null
}
}
else -> null
}
}
/**
* 通过uri生成http请求体
*/
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
Timber.d("genRequestPart imageUri = $imageUri")
// 1. 从Uri获取文件
val file = getFileFromUri(context, imageUri)
if (file == null) {
Timber.e("getFileFromUri file is null")
return null
}
// 2. 创建请求体
val requestFile = file
.asRequestBody("application/octet-stream".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData(
"file",
file.name,
requestFile
)
return imagePart
}
/**
* 通过Uri删除文件
* @param context 上下文
* @param uri 文件Uri
* @return Boolean 是否删除成功
*/
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
Timber.d("deleteFileWithUri uri = ${uri.scheme}")
return when {
// 1. 处理 content:// 类型的Uri (MediaStore)
uri.scheme.equals("content", ignoreCase = true) -> {
deleteContentUriFile(context, uri)
}
// 2. 处理 file:// 类型的Uri
uri.scheme.equals("file", ignoreCase = true) -> {
deleteFileUriFile(uri)
}
// 3. 其他情况尝试直接解析路径
else -> {
deleteFileFromPath(uri.path ?: return false)
}
}
}
// 删除Content Uri文件
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
Timber.d("deleteContentUriFile uri = ${uri.scheme}")
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// Android 10+需要特殊处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
deleteMediaStoreFile(context, uri)
} else {
false
}
} catch (e: Exception) {
Timber.e(e)
false
}
}
// Android 10+删除MediaStore文件
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
Timber.d("deleteMediaStoreFile uri = ${uri.scheme}")
val contentResolver = context.contentResolver
val projection = arrayOf(MediaStore.MediaColumns._ID)
return try {
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val contentUri = ContentUris.withAppendedId(uri, id)
contentResolver.delete(contentUri, null, null) > 0
} else {
false
}
} ?: false
} catch (e: Exception) {
Timber.e(e)
false
}
}
// 删除File Uri文件
private fun deleteFileUriFile(uri: Uri): Boolean {
Timber.d("deleteFileUriFile uri = $uri")
return try {
File(uri.path ?: return false).delete()
} catch (e: Exception) {
Timber.e(e)
false
}
}
// 直接通过路径删除文件
private fun deleteFileFromPath(path: String): Boolean {
Timber.d("deleteFileFromPath path = $path")
return try {
File(path).delete()
} catch (e: Exception) {
Timber.e(e)
false
}
}
}
@@ -0,0 +1,123 @@
package com.sw.dualscreen.utils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
object GsonUtils {
// 默认的 Gson 实例
private val defaultGson: Gson by lazy {
GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss") // 设置日期格式
// .disableHtmlEscaping() // 禁止转义HTML标签
.create()
}
/**
* 获取默认配置的 Gson 实例
*/
fun getGson(): Gson = defaultGson
/**
* 将对象转换为 JSON 字符串
* @param obj 要转换的对象
* @return JSON 字符串
*/
fun toJson(obj: Any?): String {
return if (obj == null) "" else defaultGson.toJson(obj)
}
/**
* 将 JSON 字符串转换为对象
* @param json JSON 字符串
* @param clazz 目标类
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, clazz: Class<T>): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, clazz)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为对象 (支持泛型)
* @param json JSON 字符串
* @param type 类型令牌,用于获取泛型类型
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, type: Type): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 List 对象
* @param json JSON 字符串
* @param clazz List 中的元素类型
* @return 转换后的 List 对象
*/
fun <T> fromJsonList(json: String?, clazz: Class<T>): List<T>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(List::class.java, clazz).type
defaultGson.fromJson<List<T>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 Map 对象
* @param json JSON 字符串
* @param keyClazz Map 的 key 类型
* @param valueClazz Map 的 value 类型
* @return 转换后的 Map 对象
*/
fun <K, V> fromJsonMap(
json: String?,
keyClazz: Class<K>,
valueClazz: Class<V>
): Map<K, V>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type
defaultGson.fromJson<Map<K, V>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将对象转换为另一种类型的对象
* @param obj 源对象
* @param clazz 目标类型
* @return 转换后的对象
*/
fun <T> convert(obj: Any?, clazz: Class<T>): T? {
if (obj == null) {
return null
}
return fromJson(toJson(obj), clazz)
}
}
@@ -0,0 +1,111 @@
package com.sw.inbound.utils
import android.content.Context
import androidx.core.content.edit
import com.sw.plate.App
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SPUtil private constructor(context: Context, private val spName: String) {
companion object {
@Volatile
private var instance: SPUtil? = null
fun getInstance(
context: Context = App.getContext(),
spName: String = "default_sp"
): SPUtil {
return instance ?: synchronized(this) {
instance ?: SPUtil(context.applicationContext, spName).also { instance = it }
}
}
}
private val sharedPreferences by lazy {
context.getSharedPreferences(spName, Context.MODE_PRIVATE)
}
// 基础存储方法
fun put(key: String, value: Any?) {
when (value) {
null -> remove(key) // 存入null视为删除
is String -> sharedPreferences.edit { putString(key, value) }
is Int -> sharedPreferences.edit { putInt(key, value) }
is Long -> sharedPreferences.edit { putLong(key, value) }
is Float -> sharedPreferences.edit { putFloat(key, value) }
is Boolean -> sharedPreferences.edit { putBoolean(key, value) }
is Set<*> -> sharedPreferences.edit { putStringSet(key, value as Set<String>) }
else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}")
}
notifyDataChanged(key)
}
@Suppress("UNCHECKED_CAST")
fun <T> get(key: String, defaultValue: T? = null): T? {
return when (defaultValue) {
is String -> sharedPreferences.getString(key, defaultValue) as T
is Int -> sharedPreferences.getInt(key, defaultValue) as T
is Long -> sharedPreferences.getLong(key, defaultValue) as T
is Float -> sharedPreferences.getFloat(key, defaultValue) as T
is Boolean -> sharedPreferences.getBoolean(key, defaultValue) as T
is Set<*> -> sharedPreferences.getStringSet(key, defaultValue as Set<String>) as T
null -> when {
sharedPreferences.contains(key) -> get(key, "") as? T // 尝试作为String获取
else -> null
}
else -> throw IllegalArgumentException("Unsupported type: ${defaultValue.javaClass.name}")
}
}
fun remove(key: String) {
if (sharedPreferences.contains(key)) {
sharedPreferences.edit { remove(key) }
notifyDataChanged(key)
}
}
fun clear() {
sharedPreferences.edit { clear() }
notifyDataChanged(null)
}
fun contains(key: String): Boolean {
return sharedPreferences.contains(key)
}
// 监听变化
private val dataChangeFlow = MutableStateFlow(0)
private fun notifyDataChanged(key: String?) {
dataChangeFlow.value++
}
fun observeKey(key: String): Flow<Any?> {
return dataChangeFlow.map { get(key) }
}
// 属性委托支持
fun int(key: String, default: Int = 0) = SpProperty(key, default)
fun long(key: String, default: Long = 0L) = SpProperty(key, default)
fun float(key: String, default: Float = 0f) = SpProperty(key, default)
fun boolean(key: String, default: Boolean = false) = SpProperty(key, default)
fun string(key: String, default: String = "") = SpProperty(key, default)
fun stringSet(key: String, default: Set<String> = emptySet()) = SpProperty(key, default)
inner class SpProperty<T>(private val key: String, private val defaultValue: T) :
ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return get(key, defaultValue) ?: defaultValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
put(key, value)
}
}
}
@@ -0,0 +1,123 @@
package com.sw.dualscreen.view
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.GenericItemAdapter
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
import com.sw.dualscreen.adapter.dpToPx
import com.sw.dualscreen.databinding.BottomSheetDialogBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.model.response.FoodInfo
class CustomBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var binding: BottomSheetDialogBinding
private lateinit var adapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
private var checkedItem: FoodInfo? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = BottomSheetDialogBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.ivSearch.setOnClickListener {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.BottomSheetDialogTheme)
setDialogStyle()
}
private fun setDialogStyle() {
dialog?.window?.let {
it.decorView.background = ColorDrawable(Color.TRANSPARENT)
it.decorView.setPadding(0, 0, 0, 0)
it.attributes?.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.MATCH_PARENT
}
it.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
}
companion object {
fun newInstance(): CustomBottomSheetDialog {
return CustomBottomSheetDialog()
}
}
private fun initView() {
adapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(6),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val totalItems = layoutManager.itemCount
// if (!viewModel.isLoading && viewModel.canLoadMore
// && lastVisibleItem >= totalItems - 3
// ) {
// viewModel.getSearchMemberList(
// param = lastText,
// pageNum = viewModel.currentPage
// )
// }
}
})
}
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchFoodInfoBinding::inflate,
bindCallback = { item, position ->
// this.tvName.text = item.name.maskName()
// this.tvPhone.text = item.phone.maskPhone()
// if (item.id != checkedItem?.id) {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
// } else {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
// }
//
// this.llRoot.setOnClickListener {
// Timber.d("itemClick ${item.name}, position = $position")
// checkedItem = item
// adapter.notifyDataSetChanged()
// }
}
)
}
}
@@ -0,0 +1,129 @@
package com.sw.dualscreen.view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import com.sw.plate.utils.AppUtil;
public class CustomImageView extends androidx.appcompat.widget.AppCompatImageView {
private AlphaAnimation alphaAnimation;
private String textToDraw;
private Paint mPaint;
private int mTextSize = dpToPixels(26);
private boolean isDrawText = false;
public CustomImageView(Context context) {
super(context);
init();
}
public CustomImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
textToDraw = "超标";
mPaint = new Paint();//Paint.ANTI_ALIAS_FLAG
mPaint.setColor(Color.WHITE); // Set your desired color
mPaint.setTextSize(mTextSize); // Set your desired text size
mPaint.setAntiAlias(true);
// Typeface typeface = getResources().getFont(R.font.dakai);
// Typeface typeface = Typeface.create(Typeface.createFromAsset(getContext().getAssets(), "fonts/dakai.TTF"),
// Typeface.BOLD); // 创建Typeface
// mPaint.setTypeface(typeface); // 设置Paint的Typeface
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isDrawText) {
Rect mBounds = new Rect();
mPaint.getTextBounds(textToDraw, 0, textToDraw.length(), mBounds);
mPaint.setShadowLayer(5, 2, 2, Color.GRAY);
float textWidth = mBounds.width();
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
float fontHeight = fontMetrics.bottom - fontMetrics.top;
//设置文字居中
float textBaseY = getHeight() - (getHeight() - fontHeight) / 2 - fontMetrics.bottom;
// textBaseY = 26;
canvas.drawText(textToDraw, (getWidth() - textWidth) / 2, textBaseY, mPaint);
}
}
// 用于设置自定义高度的方法
public void setCustomHeight(int height, boolean isAnimation) {
ViewGroup.LayoutParams layoutParams = getLayoutParams();
int height1 = layoutParams.height;
if (height1 != height) {
if (isAnimation) {
ValueAnimator animator = ValueAnimator.ofInt(height1, height);
animator.addUpdateListener(animation -> {
// 更新视图的高度
int val = (Integer) animation.getAnimatedValue();
ViewGroup.LayoutParams params = getLayoutParams();
params.height = dpToPixels(val);
setLayoutParams(params);
});
animator.setDuration(100); // 动画持续时间500毫秒
animator.start(); // 开始动画
} else {
if (layoutParams == null) {
layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height);
} else {
layoutParams.height = dpToPixels(height);
}
setLayoutParams(layoutParams);
}
}
}
private boolean isStartAlphaAnimation = false;
public void startAlphaAnimation() {
if (!isStartAlphaAnimation) {
alphaAnimation = new AlphaAnimation(1.0f, 0.2f); // 从不透明渐变到完全透明
alphaAnimation.setDuration(420); // 设置动画持续时间,单位为毫秒
alphaAnimation.setRepeatCount(AlphaAnimation.INFINITE); // 设置动画重复次数为无限(可选)
startAnimation(alphaAnimation);
isStartAlphaAnimation = true;
}
}
public void cancelAlphaAnimation() {
if (alphaAnimation != null)
alphaAnimation.cancel();
isStartAlphaAnimation = false;
}
public void showText(String textToDraw) {
if (!AppUtil.isEmpty(textToDraw)) {
this.textToDraw = textToDraw;
}
isDrawText = true;
}
public void hideText() {
isDrawText = false;
}
private int dpToPixels(int dp) {
float scale = getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
}
@@ -0,0 +1,163 @@
package com.sw.dualscreen.view
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.SpinnerAdapter
import android.widget.TextView
import androidx.appcompat.widget.AppCompatSpinner
import androidx.core.content.ContextCompat
import com.sw.dualscreen.R
class CustomSpinner @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.spinnerStyle
) : AppCompatSpinner(context, attrs, defStyleAttr) {
private var customAdapter: CustomArrayAdapter? = null
private var arrowDrawable: Drawable? = null
private var textColor: Int = 0
private var textSize: Float = 0f
init {
initAttributes(attrs)
setupAdapter()
}
private fun initAttributes(attrs: AttributeSet?) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomSpinner)
try {
textColor = typedArray.getColor(
R.styleable.CustomSpinner_spinnerTextColor,
ContextCompat.getColor(context, android.R.color.black)
)
textSize = typedArray.getDimension(
R.styleable.CustomSpinner_spinnerTextSize,
16f
)
arrowDrawable = typedArray.getDrawable(
R.styleable.CustomSpinner_spinnerArrowDrawable
) ?: ContextCompat.getDrawable(context, R.drawable.ic_arrow_drop_down)
} finally {
typedArray.recycle()
}
}
private fun setupAdapter() {
customAdapter = CustomArrayAdapter(context, createSpinnerView())
customAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
super.setAdapter(customAdapter)
}
private fun createSpinnerView(): View {
return LinearLayout(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
// 文本视图
addView(TextView(context).apply {
layoutParams = LinearLayout.LayoutParams(
0,
ViewGroup.LayoutParams.WRAP_CONTENT,
1f
)
id = android.R.id.text1
setTextColor(textColor)
textSize = this@CustomSpinner.textSize
setPadding(16.dpToPx(), 12.dpToPx(), 8.dpToPx(), 12.dpToPx())
})
// 箭头图标
addView(ImageView(context).apply {
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
marginEnd = 8.dpToPx()
}
setImageDrawable(arrowDrawable)
contentDescription = "下拉箭头"
})
}
}
override fun setAdapter(adapter: SpinnerAdapter?) {
if (adapter is CustomArrayAdapter) {
super.setAdapter(adapter)
customAdapter = adapter
} else {
customAdapter?.clear()
adapter?.let {
for (i in 0 until it.count) {
customAdapter?.add(it.getItem(i)?.toString() ?: "")
}
}
}
}
fun setItems(items: List<String>) {
customAdapter?.run {
clear()
addAll(items.toMutableList()) // 确保传入可变集合
notifyDataSetChanged()
}
}
fun setItems(items: Array<String>) {
setItems(items.toList())
}
fun setArrowDrawable(drawable: Drawable) {
arrowDrawable = drawable
setupAdapter() // 重新初始化适配器以应用新图标
}
fun setArrowDrawable(resId: Int) {
ContextCompat.getDrawable(context, resId)?.let {
setArrowDrawable(it)
}
}
override fun getSelectedItem(): String? {
return if (selectedItemPosition in 0 until (customAdapter?.count ?: 0)) {
customAdapter?.getItem(selectedItemPosition)
} else {
null
}
}
private inner class CustomArrayAdapter(
context: Context,
private val spinnerView: View
) : ArrayAdapter<String>(context, 0) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: spinnerView
val textView = view.findViewById<TextView>(android.R.id.text1)
textView.text = getItem(position)
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getDropDownView(position, convertView, parent) as TextView
view.setTextColor(textColor)
view.textSize = textSize
view.setPadding(16.dpToPx(), 12.dpToPx(), 16.dpToPx(), 12.dpToPx())
return view
}
}
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
}
@@ -0,0 +1,56 @@
package com.sw.dualscreen.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class CylinderDrawView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 圆柱属性(默认红色)
var cylinderColor: Int = Color.RED
set(value) {
field = value
invalidate()
}
var cylinderWidth: Int = 120 // 默认宽度(px
set(value) {
field = value
requestLayout()
}
var cylinderHeight: Int = 360 // 默认高度(px
set(value) {
field = value
requestLayout()
}
private val paint = Paint().apply {
isAntiAlias = true // 抗锯齿
style = Paint.Style.FILL // 填充模式
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val measuredWidth = resolveSize(cylinderWidth, widthMeasureSpec)
val measuredHeight = resolveSize(cylinderHeight, heightMeasureSpec)
setMeasuredDimension(measuredWidth, measuredHeight)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paint.color = cylinderColor
// 绘制圆角矩形(半径=宽度的一半,形成圆柱效果)
val radius = cylinderWidth / 2f
canvas.drawRoundRect(
0f, 0f,
cylinderWidth.toFloat(), cylinderHeight.toFloat(),
radius, radius,
paint
)
}
}
@@ -0,0 +1,104 @@
package com.sw.dualscreen.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import com.sw.dualscreen.R
class CylinderView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val valueText: TextView
private val unitText: TextView
private val cylinderDrawView: CylinderDrawView
private val topImage: ImageView
private val bottomImage: ImageView
private val bottomText: TextView
init {
// 加载内部布局
LayoutInflater.from(context).inflate(R.layout.view_cylinder, this, true)
// 绑定控件
valueText = findViewById(R.id.valueText)
unitText = findViewById(R.id.unitText)
cylinderDrawView = findViewById(R.id.cylinderDrawView)
topImage = findViewById(R.id.topImage)
bottomImage = findViewById(R.id.bottomImage)
bottomText = findViewById(R.id.bottomText)
// 解析自定义属性
context.obtainStyledAttributes(attrs, R.styleable.CylinderView).apply {
// 顶部文字:数值+单位合并
val value = getString(R.styleable.CylinderView_valueText) ?: ""
val unit = getString(R.styleable.CylinderView_unitText) ?: ""
valueText.text = value
valueText.textSize = getDimension(R.styleable.CylinderView_valueTextSize, 20f)
valueText.setTextColor(
getColor(
R.styleable.CylinderView_valueTextColor,
0xFFFFFFFF.toInt()
)
)
unitText.text = unit
unitText.textSize = getDimension(R.styleable.CylinderView_valueTextSize, 10f)
unitText.setTextColor(
getColor(
R.styleable.CylinderView_valueTextColor,
0xFFFFFFFF.toInt()
)
)
// 圆柱属性
cylinderDrawView.cylinderColor =
getColor(R.styleable.CylinderView_cylinderColor, Color.RED)
cylinderDrawView.cylinderWidth =
getDimensionPixelSize(R.styleable.CylinderView_cylinderWidth, 60)
cylinderDrawView.cylinderHeight =
getDimensionPixelSize(R.styleable.CylinderView_cylinderHeight, 180)
getResourceId(R.styleable.CylinderView_topImageSrc, -1).takeIf { it != -1 }?.let {
topImage.setImageResource(it)
// val layoutParams = cylinderDrawView.layoutParams
// layoutParams.height = (layoutParams.width / 2.45).toInt()
// topImage.layoutParams = layoutParams
}
// 底部图片
getResourceId(R.styleable.CylinderView_bottomImageSrc, -1).takeIf { it != -1 }?.let {
bottomImage.setImageResource(it)
// val layoutParams = cylinderDrawView.layoutParams
// layoutParams.height = 44
// bottomImage.layoutParams = layoutParams
}
// 底部文字
bottomText.text = getString(R.styleable.CylinderView_bottomText) ?: "热量"
bottomText.textSize = getDimension(R.styleable.CylinderView_bottomTextSize, 16f)
bottomText.setTextColor(
getColor(
R.styleable.CylinderView_bottomTextColor,
0xFFFFFFFF.toInt()
)
)
recycle() // 回收属性
}
}
fun setToImageSize() {
// topImage.layoutParams = LayoutParams.
}
fun setBottomSize() {
}
}
@@ -0,0 +1,75 @@
package com.sw.dualscreen.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.response.ApiResponse
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.network.ApiClient
import com.sw.dualscreen.repository.RemoteRepository
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class BaseViewModel() : ViewModel() {
protected val repository = RemoteRepository(ApiClient.apiService)
// 显示进度条
private val _showLoading = MutableStateFlow<Boolean>(false)
val showLoading: StateFlow<Boolean> = _showLoading
/**
* 带进度条的请求
*/
protected fun launchWithLoading(block: suspend () -> Unit) {
viewModelScope.launch {
try {
_showLoading.value = true
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
} finally {
_showLoading.value = false
}
}
}
/**
* 不带进度条的请求
*/
protected fun launch(block: suspend () -> Unit) {
viewModelScope.launch() {
try {
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
}
}
}
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()) {
return true
}
Timber.d("msg = ${response.message}, code = ${response.code}")
ToastUtils.showToast("${response.message}(${response.code})")
return false
}
fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
GlobalData.appId = equipmentInfo.arcsoftAppId!!
// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
GlobalData.restId = equipmentInfo.canteenId!!
}
fun handleError(exception: Exception) {
Timber.d("handleError ${exception.message}")
}
}
@@ -0,0 +1,66 @@
package com.sw.dualscreen.viewmodel
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.utils.GsonUtils
import com.sw.inbound.utils.SPUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
/**
* 初始化的viewmodel
*/
class DeviceViewModel : BaseViewModel() {
private val _deviceInfoResult = MutableStateFlow<Boolean?>(null)
val deviceInfoResult: StateFlow<Boolean?> = _deviceInfoResult
/**
* 获取token
*/
fun getDeviceToken(deviceId: String = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e") {
launchWithLoading {
val response = repository.getDeviceToken(deviceId)
if (parseResponse(response)) {
val response1 = repository.getDeviceInfo(deviceId, response.result!!)
if (parseResponse(response1)) {
val equipmentInfo = response1.result
if (equipmentInfo == null) return@launchWithLoading
try {
parseEquipmentInfo(equipmentInfo)
SPUtil.getInstance()
.put(GlobalKey.KEY_EQUIPMENT_INFO, GsonUtils.toJson(equipmentInfo))
_deviceInfoResult.value = true
} catch (e: Exception) {
Timber.e(e)
}
}
}
}
}
/**
* 检查缓存数据
*/
fun checkEquipmentInfo(): Boolean {
val equipmentInfoStr = SPUtil.getInstance().get(GlobalKey.KEY_EQUIPMENT_INFO, "")
if (equipmentInfoStr == null) {
Timber.e("获取缓存设备信息失败")
return false
}
val equipmentInfo =
GsonUtils.fromJson<EquipmentInfo>(equipmentInfoStr, EquipmentInfo::class.java)
if (equipmentInfo == null) {
Timber.e("解析缓存设备信息失败")
return false
}
try {
parseEquipmentInfo(equipmentInfo)
return true
} catch (e: Exception) {
Timber.e(e)
return false
}
}
}
@@ -1,8 +0,0 @@
package com.sw.dualscreen.viewmodel
import androidx.lifecycle.ViewModel
class MyViewModel : ViewModel() {
}
@@ -0,0 +1,182 @@
package com.sw.dualscreen.viewmodel
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.NutritionData
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.plate.utils.Base64
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.FaceApi
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
class UserViewModel : BaseViewModel() {
private val faceApi: FaceApi = FaceApi()
// 识别出的列表
private val _identifiedFoodInfoList = MutableStateFlow<List<FoodInfo>?>(null)
val identifiedFoodInfoList: StateFlow<List<FoodInfo>?> = _identifiedFoodInfoList
// 搜索出的食物列表
private val _foodInfoList = MutableStateFlow<List<FoodInfo>?>(null)
val foodInfoList: StateFlow<List<FoodInfo>?> = _foodInfoList
// 当前食物信息
private val _currentFoodInfo = MutableStateFlow<FoodInfo?>(null)
val currentFoodInfo: StateFlow<FoodInfo?> = _currentFoodInfo
// 就餐数据
private val _nutritionData = MutableStateFlow<NutritionData?>(null)
val nutritionData: StateFlow<NutritionData?> = _nutritionData
/**
* 获取token
*/
fun getEquipmentToken(qrcodeId: String = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e") {
launchWithLoading {
val response = repository.getEquipmentToken(qrcodeId)
if (parseResponse(response)) {
// 缓存token
SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, response.result)
// 首次运行获取人脸数据
if (SPUtil.getInstance().get(GlobalKey.KEY_FIRST_RUN, false) != true) {
getUserFaceCache(index = 0)
}
}
}
}
/**
* 获取人脸数据
*/
fun getUserFaceCache(index: Int = 0) {
Timber.d("getUserFaceCache index = $index")
launch {
val response = repository.getUserFaceCache(index)
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.result?.data ?: emptyList()
val faceEntity = list.map {
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureString))
}
faceApi.updateFaceData(index, faceEntity)
activeEngine()
}
val nextPageIndex = response.result?.nextPageIndex ?: -1
if (nextPageIndex > 0) {
getUserFaceCache(nextPageIndex)
}
}
}
}
/**
* 激活人脸识别引擎
*/
fun activeEngine() {
Timber.d("activeEngine")
faceApi.activeEngine(
App.getContext(),
GlobalData.appId,
GlobalData.sdkKey,
GlobalData.activeKey,
object : FaceApi.ActiveCallback {
override fun onSuccess(activeCode: Int) {
Timber.d("activeEngine activeCode = $activeCode")
viewModelScope.launch(Dispatchers.Main) {
when (activeCode) {
ErrorInfo.MOK -> {
ToastUtils.showToast("激活引擎成功")
}
ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
// ToastUtils.showToast("引擎已激活,无需再次激活")
}
else -> {
ToastUtils.showToast("激活引擎失败($activeCode)")
}
}
}
}
override fun onFail(e: Exception?) {
viewModelScope.launch(Dispatchers.Main) {
ToastUtils.showToast("激活引擎异常,${e?.message}")
}
}
})
}
/**
* 重置用户信息
*/
fun resetUserInfo() {
Timber.d("resetUserInfo")
}
fun identifiedFoodList() {
launchWithLoading {
delay(5000)
_identifiedFoodInfoList.value = List(6) {
FoodInfo(
id = "185225109358109491${it}",
foodName = "豆芽炒粉条$it",
imgUrl = "http://101.201.149.156:9002/temp/20221119/62dc2d17-6853-4277-96ed-a4ef4c8654ea_1668840050258.png"
)
}
}
}
/**
* 搜索食物
*/
fun searchByFoodName(foodName: String) {
launchWithLoading {
val response = repository.getRestInfoFoodsByType(foodName = foodName)
if (parseResponse(response)) {
_foodInfoList.value = response.result
}
}
}
/**
* 获取用户就餐数据
*/
fun getUserNutritionData(userId: String, foodId: String) {
launchWithLoading {
val response = repository.getUserNutritionData(userId = userId, foodId = foodId)
if (parseResponse(response)) {
_nutritionData.value = response.result
}
}
}
/**
* 提交就餐数据
*/
fun postUserNutritionData(param: UserNutritionParam) {
launchWithLoading {
val response = repository.postUserNutritionData(param)
if (parseResponse(response)) {
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:width="2dp"
android:color="#ffece7d0" />
</shape>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#fff8f8f8" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<corners
android:topLeftRadius="16dp"
android:topRightRadius="16dp" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ffece7d0" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>
@@ -0,0 +1,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#fffffaf5" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:color="#FF9900"
android:width="2dp" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:width="2dp"
android:color="#e6e6e6" />
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ffdcdcdc" />
<corners
android:bottomLeftRadius="5dp"
android:bottomRightRadius="5dp"
android:topLeftRadius="5dp"
android:topRightRadius="5dp" />
</shape>
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:id="@+id/textureView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/switchButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="切换摄像头"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".activity.InitActivity">
<TextView
android:id="@+id/tv_deviceId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="1111" />
<Button
android:id="@+id/bt_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="获取" />
</LinearLayout>
+36 -14
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
@@ -9,31 +10,44 @@
android:orientation="vertical"
tools:context=".activity.MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="80dp"
android:layout_marginTop="40dp">
android:paddingTop="40dp"
android:paddingEnd="40dp">
<Spinner
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="80dp"
android:layout_gravity="end|center_vertical" />
android:layout_height="wrap_content"
android:layout_gravity="end"
android:padding="28dp"
android:background="@drawable/bg_detection_selector">
<com.sw.dualscreen.view.CustomSpinner
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:spinnerTextColor="#ffece7d0"
app:spinnerTextSize="26sp" />
</LinearLayout>
</FrameLayout>
<TextView
style="@style/text_title_60_blob"
android:id="@+id/tv_goods"
style="@style/text_title_blob"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="130dp"
android:text="羊肉臊子荞面饸饹" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="630dp"
android:layout_marginStart="120dp"
android:layout_marginTop="60dp"
android:layout_marginEnd="120dp"
android:layout_marginBottom="60dp"
android:layout_height="531dp"
android:layout_marginHorizontal="60dp"
android:layout_marginTop="120dp"
android:layout_marginBottom="90dp"
android:visibility="visible">
<androidx.camera.view.PreviewView
@@ -49,13 +63,21 @@
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="0dp"
android:layout_marginHorizontal="60dp"
android:layout_weight="1" />
<TextView
style="@style/text_100"
android:id="@+id/tv_to_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="先识别 后取餐1" />
android:layout_marginTop="90dp"
android:layout_marginBottom="90dp"
android:padding="10dp"
android:text="以上都不是,手动搜索"
android:textColor="#ffff9900"
android:textSize="36sp" />
</LinearLayout>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bottom_sheet_layout"
android:layout_width="match_parent"
android:layout_height="1000dp"
android:background="@drawable/bottom_sheet_background"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingHorizontal="60dp"
android:paddingTop="30dp"
android:paddingBottom="0dp">
<View
android:layout_width="90dp"
android:layout_height="10dp"
android:background="@drawable/view_shape_point" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="46dp"
android:text="菜品检索"
android:textColor="#ff333333"
android:textSize="36sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="50dp"
android:background="@drawable/bg_search"
android:gravity="center_vertical"
android:orientation="horizontal">
<EditText
android:id="@+id/et_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_weight="1"
android:background="@null"
android:hint="输入菜品名称"
android:imeOptions="actionSearch"
android:maxLines="1"
android:textColor="#333333"
android:textColorHint="#ffc8c8c8"
android:textSize="36sp" />
<ImageView
android:id="@+id/iv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="30dp"
android:src="@drawable/ic_search" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="700dp"
android:layout_marginTop="50dp" />
</LinearLayout>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fl_preview"
android:layout_width="match_parent"
android:layout_height="531dp"
android:layout_marginHorizontal="60dp"
android:layout_marginTop="120dp"
android:layout_marginBottom="90dp"
android:visibility="visible">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="30dp"
android:background="@drawable/preview_border" />
</FrameLayout>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_item_checked"
android:gravity="center"
android:orientation="vertical"
android:paddingVertical="42dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="肉沫干拌面"
android:maxLines="1"
android:ellipsize="end"
android:textColor="#ff031127"
android:textSize="36sp"
android:textStyle="bold" />
</LinearLayout>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_search_item_checked"
android:gravity="center"
android:orientation="vertical"
android:paddingVertical="42dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="肉沫干拌面"
android:maxLines="1"
android:ellipsize="end"
android:textColor="#FF9900"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
+138
View File
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="540dp"
android:layout_height="487dp"
android:background="@drawable/rounded_border_transparent"
android:padding="28dp">
<TextView
android:id="@+id/tv_user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="夏*秋"
android:textColor="#ffece7d0"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_heat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="推荐热量:680kcal"
android:textColor="#ffece7d0"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_user_name" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="40dp"
android:background="#19ECE7D0"
app:layout_constraintTop_toBottomOf="@id/tv_user_name" />
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:paddingBottom="40dp"
app:constraint_referenced_ids="fl_heat, fl_staple_food, fl_fruits_vegetables, fl_meat_eggs"
app:flow_wrapMode="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<FrameLayout
android:id="@+id/fl_heat"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.sw.dualscreen.view.CustomImageView
android:id="@+id/iv_heat"
android:layout_width="87dp"
android:layout_height="0dp"
android:layout_gravity="bottom|center"
android:layout_marginBottom="33dp"
android:background="#DB0000" />
<ImageView
android:layout_width="87dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_cylinder_red_small" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_marginBottom="33dp"
android:src="@drawable/ic_heat" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:text="热量"
android:textColor="#fff0e6dc"
android:textSize="20sp" />
</FrameLayout>
<FrameLayout
android:id="@+id/fl_staple_food"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<com.sw.dualscreen.view.CustomImageView
android:id="@+id/iv_staple_food"
android:layout_width="87dp"
android:src="#99CC99"
android:layout_gravity="bottom|center"
android:layout_height="100dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:src="@drawable/ic_staple_food" />
</FrameLayout>
<FrameLayout
android:id="@+id/fl_fruits_vegetables"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<com.sw.dualscreen.view.CustomImageView
android:id="@+id/iv_fruits_vegetables"
android:layout_width="87dp"
android:layout_gravity="bottom|center"
android:layout_height="match_parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:src="@drawable/ic_fruits_vegetables" />
</FrameLayout>
<FrameLayout
android:id="@+id/fl_meat_eggs"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<com.sw.dualscreen.view.CustomImageView
android:id="@+id/iv_meat_eggs"
android:layout_width="87dp"
android:layout_gravity="bottom|center"
android:layout_height="match_parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:src="@drawable/ic_meat_eggs" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -11,69 +11,126 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginStart="120dp"
android:layout_marginTop="120dp"
android:layout_marginEnd="120dp"
android:background="@drawable/rounded_border_transparent"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible">
android:layout_height="wrap_content"
android:paddingHorizontal="80dp"
android:paddingTop="80dp">
<TextView
android:id="@+id/tv_goods"
style="@style/text_title_60_blob"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="羊肉臊子荞面饸饹" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="133dp"
android:background="@drawable/rounded_border_transparent"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/tv_goods"
style="@style/text_title_blob_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="羊肉臊子荞面饸饹" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="560dp"
android:layout_marginTop="60dp"
android:layout_height="match_parent"
android:layout_marginTop="40dp"
android:visibility="visible">
<FrameLayout
android:layout_width="560dp"
android:id="@+id/fl_face"
android:layout_width="376dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingTop="1dp">
android:paddingTop="1dp"
android:visibility="gone">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="380dp" />
android:layout_height="match_parent" />
</FrameLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/iv_face_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/bg_face" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="730dp"
android:layout_marginStart="120dp"
android:layout_marginEnd="120dp"
android:layout_marginBottom="120dp"
android:background="@drawable/rounded_border_transparent"
android:gravity="center">
android:background="@drawable/bg_avatar"
android:visibility="gone" />
<TextView
style="@style/text_100"
android:id="@+id/tv_face_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="先识别 后取餐" />
</LinearLayout>
android:layout_gravity="center_horizontal"
android:layout_marginTop="376dp"
android:text="请正视屏幕"
android:textColor="#ffece7d0"
android:textSize="24sp"
android:visibility="gone" />
<FrameLayout
android:id="@+id/fl_preview"
android:layout_width="match_parent"
android:layout_height="420dp"
android:visibility="visible"
android:paddingHorizontal="80dp">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
android:contentDescription="img" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:background="@drawable/preview_border" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:paddingHorizontal="80dp"
android:paddingBottom="80dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="486dp"
android:background="@drawable/rounded_border_transparent"
android:gravity="center">
<TextView
android:id="@+id/tv_bottom_tip"
style="@style/text_100_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="先识别 后取餐"
android:visibility="gone" />
<include
android:id="@+id/include_user_heat"
layout="@layout/item_user_heat"
android:visibility="visible" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>
+71
View File
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:gravity="center"
android:orientation="vertical"
android:padding="8dp"> <!-- 深蓝色背景 -->
<!-- 顶部文字:数值+单位 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/valueText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textColor="@android:color/white"
android:textSize="20sp" />
<TextView
android:id="@+id/unitText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textColor="@android:color/white"
android:textSize="10sp" />
</LinearLayout>
<!-- 圆柱容器 -->
<FrameLayout
android:id="@+id/cylinderContainer"
android:layout_width="87dp"
android:layout_height="wrap_content">
<!-- 圆柱绘制View -->
<com.sw.dualscreen.view.CylinderDrawView
android:id="@+id/cylinderDrawView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal" />
<ImageView
android:id="@+id/topImage"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="top|center_horizontal"
android:background="@drawable/ic_cylinder_red_small" />
<!-- 底部火焰图片(覆盖圆柱底部) -->
<ImageView
android:id="@+id/bottomImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:src="@drawable/ic_heat" /> <!-- 替换为你的火焰图片资源 -->
</FrameLayout>
<!-- 底部文字:热量 -->
<TextView
android:id="@+id/bottomText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="热量"
android:textColor="@android:color/white"
android:textSize="16sp" />
</LinearLayout>
+30
View File
@@ -10,4 +10,34 @@
<declare-styleable name="RoundedPreviewView">
<attr name="cornerRadius" format="dimension" />
</declare-styleable>
<declare-styleable name="CustomSpinner">
<attr name="spinnerTextColor" format="color" />
<attr name="spinnerTextSize" format="dimension" />
<attr name="spinnerArrowDrawable" format="reference" />
<attr name="spinnerBackground" format="reference" />
<attr name="spinnerDropDownBackground" format="reference" />
</declare-styleable>
<!--热量圆柱-->
<declare-styleable name="CylinderView">
<!-- 数值+单位文字 -->
<attr name="valueText" format="string" />
<attr name="unitText" format="string" />
<attr name="valueTextSize" format="dimension" />
<attr name="valueTextColor" format="color" />
<!-- 圆柱属性 -->
<attr name="cylinderColor" format="color" />
<attr name="cylinderWidth" format="dimension" />
<attr name="cylinderHeight" format="dimension" />
<!-- 底部图片 -->
<attr name="topImageSrc" format="reference" />
<attr name="bottomImageSrc" format="reference" />
<!-- 底部文字 -->
<attr name="bottomText" format="string" />
<attr name="bottomTextSize" format="dimension" />
<attr name="bottomTextColor" format="color" />
</declare-styleable>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="bottom_sheet_height">1000dp</dimen>
</resources>
+5
View File
@@ -2,4 +2,9 @@
<string name="app_name">档口双屏</string>
<string name="specific_engine_init_failed">%s 初始化失败,错误码:%d\n错误码常量名:%s</string>
<string name="permission_denied">权限被拒绝!</string>
<array name="detection_method">
<item>即放即取</item>
<item>持续取餐</item>
</array>
</resources>
+37 -1
View File
@@ -7,7 +7,7 @@
<style name="Theme.DualScreen" parent="Base.Theme.DualScreen" />
<style name="text_title_60_blob">
<style name="text_title_blob">
<item name="android:layout_width">match_parent</item>
<item name="android:textColor">@color/beige_ECE7D0</item>
<item name="android:textSize">60sp</item>
@@ -15,6 +15,14 @@
<item name="singleLine">true</item>
</style>
<style name="text_title_blob_2">
<item name="android:layout_width">match_parent</item>
<item name="android:textColor">@color/beige_ECE7D0</item>
<item name="android:textSize">53sp</item>
<item name="android:textStyle">bold</item>
<item name="singleLine">true</item>
</style>
<style name="text_100">
<item name="android:layout_width">match_parent</item>
<item name="android:textColor">@color/origin_FF9900</item>
@@ -24,4 +32,32 @@
<item name="maxLines">1</item>
<item name="android:ellipsize">end</item>
</style>
<style name="text_100_2">
<item name="android:layout_width">match_parent</item>
<item name="android:textColor">@color/origin_FF9900</item>
<item name="android:textSize">67sp</item>
<item name="android:textStyle">bold</item>
<item name="singleLine">true</item>
<item name="maxLines">1</item>
<item name="android:ellipsize">end</item>
</style>
<style name="BottomSheetDialogTheme" parent="Theme.Design.Light.BottomSheetDialog">
<item name="bottomSheetStyle">@style/BottomSheetStyle</item>
</style>
<style name="BottomSheetStyle" parent="Widget.Design.BottomSheet.Modal">
<!-- 移除默认边距 -->
<item name="android:layout_marginLeft">0dp</item>
<item name="android:layout_marginRight">0dp</item>
<item name="android:layout_marginBottom">0dp</item>
<!-- 设置背景透明,避免默认白色背景遮挡圆角 -->
<item name="android:background">@android:color/transparent</item>
</style>
<style name="SpinnerTextAppearance" parent="android:TextAppearance.Widget.TextView.SpinnerItem">
<item name="android:textSize">26sp</item>
<item name="android:textColor">#ffece7d0</item>
</style>
</resources>
+1 -1
View File
@@ -52,7 +52,7 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation("com.licheedev:android-serialport:2.1.5")
// implementation("com.licheedev:android-serialport:2.1.5")
val roomVersion = "2.2.5"
implementation("androidx.room:room-runtime:$roomVersion")
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,148 @@
package com.sw.plate.sdk
import android.util.Log
import com.sw.plate.utils.ThreadUtils
import com.sw.plate.utils.ToastUtils
import com.wabon.wbintelligenthardwaresdk.api.SensorScale
import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult
import kotlinx.coroutines.delay
typealias Callback = (Double) -> Unit
private const val TAG = "SensorScaleUtils"
/**
* 称 传感器计算
*/
object SensorScaleUtils {
const val serialPort = "/dev/ttyS7"
const val baudRate = 115200
private var mSensorScale: SensorScale? = null
private var isOpened: Boolean = false
private var callback: Callback? = {}
private fun init() {
mSensorScale = SensorScale(object : OnScaleResult {
/**
* 读取重量
*/
override fun readWeight(state: Int, value: Double) {
val stateStr = when (state) {
SensorScale.STATE_STABLE -> "稳定"
SensorScale.STATE_UNSTABLE -> "不稳定"
SensorScale.STATE_OVER_WEIGHT -> "量程溢出"
else -> "未知"
}
// Log.d(TAG, "readWeight state = ${stateStr}, weight = $value")
// 只使用稳定值
if (state == SensorScale.STATE_STABLE) {
callback?.invoke(value)
}
}
/**
* 读取鉴别率
*/
override fun readIdentify(rate: Int) {
Log.e(TAG, "readIdentify rate = $rate")
}
override fun fail(errCode: Int) {
Log.e(TAG, "fail code = $errCode")
}
})
}
/**
* 开启称重
* @param autoScale 是否开启自动读取
*/
fun startScale(autoScale: Boolean = true, callback: Callback?) {
if (isOpened) {
startContinuousRead(callback = callback)
return
}
this.callback = callback
init()
mSensorScale?.openScale(serialPort, baudRate) { open ->
isOpened = open
Log.d(TAG, "isOpened = $isOpened")
if (open) {
if (autoScale) {
ThreadUtils.launchOnIo {
delay(1000)
// 打开后需要等待后才能调用,否则会 1001 SDK未初始化
mSensorScale?.startContinuousRead()
}
}
}
}
}
/**
* 开启自动读取重量
*/
fun startContinuousRead(callback: Callback?) {
this.callback = callback
Log.d(TAG, "开启自动读取 = $isOpened")
if (!isOpened) {
return
}
mSensorScale?.startContinuousRead()
}
/**
* 手动读取重量
*/
fun readWeight(callback: Callback?) {
this.callback = callback
Log.d(TAG, "isOpened = $isOpened")
if (!isOpened) {
return
}
mSensorScale?.readWeight()
}
/**
* 零位标定
*/
fun zero() {
if (!isOpened) return
mSensorScale?.zero {
Log.d(TAG, "零位标定操作成功")
ToastUtils.showToast("零位标定操作成功")
}
}
/**
* 去皮置零
*/
fun tare() {
if (!isOpened) return
mSensorScale?.tare {
Log.d(TAG, "去皮置零操作成功")
ToastUtils.showToast("去皮置零操作成功")
}
}
/**
* 停止自动读取重量
*/
fun stopContinuousRead() {
Log.d(TAG, "isOpened = $isOpened")
if (!isOpened) {
return
}
mSensorScale?.stopContinuousRead()
}
/**
* 关闭称重
*/
fun closeScale() {
isOpened = false
mSensorScale?.closeScale()
mSensorScale == null
}
}
@@ -0,0 +1,124 @@
package com.sw.plate.utils
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext
/**
* 多功能线程工具类
* 结合协程、Handler和线程池实现线程切换
*/
object ThreadUtils : CoroutineScope {
// 主线程Handler
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
// 后台线程池(IO密集型任务)
private val ioThreadPool: ExecutorService by lazy {
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
}
// CPU密集型线程池
private val cpuThreadPool: ExecutorService by lazy {
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
}
// 协程Job管理
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// ========== Handler相关方法 ==========
/**
* 在主线程执行任务
* @param delayMillis 延迟时间(毫秒)
*/
fun runOnUiThread(delayMillis: Long = 0, block: () -> Unit) {
if (delayMillis > 0) {
mainHandler.postDelayed(block, delayMillis)
} else {
if (isOnMainThread()) {
block()
} else {
mainHandler.post(block)
}
}
}
/**
* 移除主线程任务
*/
fun removeUiThreadTask(block: () -> Unit) {
mainHandler.removeCallbacks(block)
}
// ========== 线程池相关方法 ==========
/**
* 在IO线程执行任务
*/
fun runOnIoThread(block: () -> Unit) {
ioThreadPool.execute(block)
}
/**
* 在CPU计算线程执行任务
*/
fun runOnCpuThread(block: () -> Unit) {
cpuThreadPool.execute(block)
}
// ========== 协程相关方法 ==========
/**
* 启动协程(默认在主线程)
*/
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return launch(coroutineContext, block = block)
}
/**
* 在IO线程启动协程
*/
fun launchOnIo(block: suspend CoroutineScope.() -> Unit): Job {
return launch(Dispatchers.IO, block = block)
}
/**
* 切换到主线程(协程环境)
*/
suspend fun <T> switchToMain(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.Main, block)
}
/**
* 切换到IO线程(协程环境)
*/
suspend fun <T> switchToIo(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.IO, block)
}
/**
* 是否在主线程
*/
fun isOnMainThread(): Boolean {
return Looper.myLooper() == Looper.getMainLooper()
}
/**
* 释放资源
*/
fun release() {
job.cancel()
ioThreadPool.shutdown()
cpuThreadPool.shutdown()
}
}
@@ -268,16 +268,19 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
/**
* 初始化引擎
*/
public void init() {
public void init(PreviewConfig previewConfig1) {
Context context = App.getContext();
boolean switchCamera = ConfigUtil.isSwitchCamera(context);
previewConfig = new PreviewConfig(
switchCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK,
switchCamera ? Camera.CameraInfo.CAMERA_FACING_BACK : Camera.CameraInfo.CAMERA_FACING_FRONT,
Integer.parseInt(ConfigUtil.getRgbCameraAdditionalRotation(context)),
Integer.parseInt(ConfigUtil.getIrCameraAdditionalRotation(context))
);
if (previewConfig1 != null) {
previewConfig = previewConfig1;
} else {
boolean switchCamera = ConfigUtil.isSwitchCamera(context);
previewConfig = new PreviewConfig(
switchCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK,
switchCamera ? Camera.CameraInfo.CAMERA_FACING_BACK : Camera.CameraInfo.CAMERA_FACING_FRONT,
Integer.parseInt(ConfigUtil.getRgbCameraAdditionalRotation(context)),
Integer.parseInt(ConfigUtil.getIrCameraAdditionalRotation(context))
);
}
// 填入在设置界面设置好的配置信息
boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable));
boolean enableFaceQualityDetect = ConfigUtil.isEnableImageQualityDetect(context);
@@ -1,39 +0,0 @@
package com.sw.plate.utils.comn;
/**
* 串口设备
*/
public class Device {
private String path;
private String baudrate;
public Device() {
}
public Device(String path, String baudrate) {
this.path = path;
this.baudrate = baudrate;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getBaudrate() {
return baudrate;
}
public void setBaudrate(String baudrate) {
this.baudrate = baudrate;
}
@Override
public String toString() {
return "Device{" + "path='" + path + '\'' + ", baudrate='" + baudrate + '\'' + '}';
}
}
@@ -1,37 +0,0 @@
package com.sw.plate.utils.comn;
import static com.sw.plate.utils.CabinetLockCommand.generateOpenCommand;
import android.serialport.SerialPort;
public class SerialApi {
private static String path = "/dev/ttyS2";
private static int speed = 19200;
private static SerialPortManager serialPortManager;
private static SerialPort serialPort;
public static void init() {
serialPortManager = SerialPortManager.instance();
serialPort = serialPortManager.open(new Device(path, String.valueOf(speed)));
// if (serialPort == null) {
// ToastUtils.showToast("打开串口失败");
// }
}
/**
* 开柜
*
* @param boxNumber
* @param callback
*/
public static void openPlate(int boxNumber, SerialPortManager.SendCallback callback) {
if (serialPort == null) {
init();
}
if (serialPort == null) {
callback.onFail(new Exception("打开串口失败"));
return;
}
serialPortManager.sendCommand(generateOpenCommand(boxNumber), callback);
}
}
@@ -1,192 +0,0 @@
package com.sw.plate.utils.comn;
import android.os.HandlerThread;
import android.serialport.SerialPort;
import com.sw.plate.utils.ByteUtil;
import com.sw.plate.utils.L;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.Observer;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
/**
* Created by Administrator on 2017/3/28 0028.
*/
public class SerialPortManager {
private static final String TAG = "SerialPortManager";
private SerialReadThread mReadThread;
private OutputStream mOutputStream;
private HandlerThread mWriteThread;
private Scheduler mSendScheduler;
private static class InstanceHolder {
public static SerialPortManager sManager = new SerialPortManager();
}
public static SerialPortManager instance() {
return InstanceHolder.sManager;
}
private SerialPort mSerialPort;
private SerialPortManager() {
}
/**
* 打开串口
*
* @param device
* @return
*/
public SerialPort open(Device device) {
return open(device.getPath(), device.getBaudrate());
}
/**
* 打开串口
*
* @param devicePath
* @param baudrateString
* @return
*/
public SerialPort open(String devicePath, String baudrateString) {
if (mSerialPort != null) {
close();
}
try {
File device = new File(devicePath);
int baurate = Integer.parseInt(baudrateString);
mSerialPort = new SerialPort(device, baurate);
mReadThread = new SerialReadThread(mSerialPort.getInputStream());
mReadThread.start();
mOutputStream = mSerialPort.getOutputStream();
mWriteThread = new HandlerThread("write-thread");
mWriteThread.start();
mSendScheduler = AndroidSchedulers.from(mWriteThread.getLooper());
L.e("串口打开成功");
return mSerialPort;
} catch (Throwable tr) {
L.e("打开串口失败" + tr);
close();
return null;
}
}
/**
* 关闭串口
*/
public void close() {
if (mReadThread != null) {
mReadThread.close();
}
if (mOutputStream != null) {
try {
mOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (mWriteThread != null) {
mWriteThread.quit();
}
if (mSerialPort != null) {
mSerialPort.close();
mSerialPort = null;
}
}
/**
* 发送数据
*
* @param datas
* @return
*/
private void sendData(byte[] datas) throws Exception {
mOutputStream.write(datas);
}
/**
* (rx包裹)发送数据
*
* @param datas
* @return
*/
private Observable<Object> rxSendData(final byte[] datas) {
return Observable.create(new ObservableOnSubscribe<Object>() {
@Override
public void subscribe(ObservableEmitter<Object> emitter) throws Exception {
try {
sendData(datas);
emitter.onNext(new Object());
} catch (Exception e) {
L.e("发送:" + ByteUtil.bytes2HexStr(datas) + " 失败===" + e);
if (!emitter.isDisposed()) {
emitter.onError(e);
return;
}
}
emitter.onComplete();
}
});
}
/**
* 发送命令包
*/
public void sendCommand(final String command, SendCallback callback) {
// TODO: 2018/3/22
L.e("发送命令:" + command);
byte[] bytes = ByteUtil.hexStr2bytes(command);
rxSendData(bytes).subscribeOn(mSendScheduler).subscribe(new Observer<Object>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(Object o) {
// LogManager.instance().post(new SendMessage(command));
callback.onSuccess();
}
@Override
public void onError(Throwable e) {
L.e("发送失败" + e);
callback.onFail(new Exception(e));
}
@Override
public void onComplete() {
}
});
}
public interface SendCallback {
void onSuccess();
void onFail(Exception e);
}
}
@@ -1,96 +0,0 @@
package com.sw.plate.utils.comn;
import static com.sw.plate.utils.CabinetLockCommand.parseBoxStatus;
import android.os.SystemClock;
import com.sw.plate.utils.ByteUtil;
import com.sw.plate.utils.L;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
/**
* 读串口线程
*/
public class SerialReadThread extends Thread {
private static final String TAG = "SerialReadThread";
private BufferedInputStream mInputStream;
public SerialReadThread(InputStream is) {
mInputStream = new BufferedInputStream(is);
}
@Override
public void run() {
byte[] received = new byte[1024];
int size;
L.e("开始读线程");
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
try {
int available = mInputStream.available();
if (available > 0) {
size = mInputStream.read(received);
if (size > 0) {
onDataReceive(received, size);
}
} else {
// 暂停一点时间,免得一直循环造成CPU占用率过高
SystemClock.sleep(1);
}
} catch (IOException e) {
L.e("读取数据失败" + e);
}
//Thread.yield();
}
L.e("结束读进程");
}
/**
* 处理获取到的数据
*
* @param received
* @param size
*/
private void onDataReceive(byte[] received, int size) {
// TODO: 2018/3/22 解决粘包、分包等
String hexStr = ByteUtil.bytes2HexStr(received, 0, size);
// LogManager.instance().post(new RecvMessage(hexStr));
L.e("接收数据:" + hexStr);
if (hexStr.startsWith("5AA2")) {
Map<Integer, Boolean> statusMap = parseBoxStatus(hexStr);
for (Map.Entry<Integer, Boolean> entry : statusMap.entrySet()) {
System.out.println("箱门" + entry.getKey() + ": " +
(entry.getValue() ? "" : ""));
}
}
}
/**
* 停止读线程
*/
public void close() {
try {
mInputStream.close();
} catch (IOException e) {
L.e("异常" + e);
} finally {
super.interrupt();
}
}
}
@@ -1,21 +0,0 @@
package com.sw.plate.utils.comn.message;
/**
* 日志消息数据接口
*/
public interface IMessage {
/**
* 消息文本
*
* @return
*/
String getMessage();
/**
* 是否发送的消息
*
* @return
*/
boolean isToSend();
}
@@ -1,26 +0,0 @@
package com.sw.plate.utils.comn.message;
/**
* 收到的日志
*/
public class RecvMessage implements IMessage {
private String command;
private String message;
public RecvMessage(String command) {
this.command = command;
this.message = " 收到命令:" + command;
}
@Override
public String getMessage() {
return message;
}
@Override
public boolean isToSend() {
return false;
}
}
@@ -1,26 +0,0 @@
package com.sw.plate.utils.comn.message;
/**
* 发送的日志
*/
public class SendMessage implements IMessage {
private String command;
private String message;
public SendMessage(String command) {
this.command = command;
this.message = " 发送命令:" + command;
}
@Override
public String getMessage() {
return message;
}
@Override
public boolean isToSend() {
return true;
}
}