实现了双屏摄像头显示,添加了部分代码
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="bottom_sheet_height">1000dp</dimen>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||