Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9818dfc438 | ||
|
|
cb9d68d2e3 | ||
|
|
b4087e435e | ||
|
|
4abc855b36 | ||
|
|
c898cbb3e2 | ||
|
|
0a09814a61 | ||
|
|
cc2fd2d743 | ||
|
|
cd883af92f | ||
|
|
9c60049843 | ||
|
|
e47b362883 | ||
|
|
ed6c5e83be | ||
|
|
a27cc5f7ee | ||
|
|
dd51448aea | ||
|
|
b62738aa15 | ||
|
|
7a60f4b34d | ||
|
|
c23aedf464 | ||
|
|
795e685a3a | ||
|
|
d9b725cb63 | ||
|
|
bb63b47b97 | ||
|
|
caefbd0944 | ||
|
|
d48f41e917 | ||
|
|
18fbdc9875 | ||
|
|
f055441340 | ||
|
|
e93550a24b | ||
|
|
fde2cd0544 | ||
|
|
fc326a8f42 | ||
|
|
4086f9c0b7 | ||
|
|
4a29b3adc9 | ||
|
|
78a682b664 | ||
|
|
2e20e7deab | ||
|
|
c3489ab9e6 | ||
|
|
1f1c3804ac | ||
|
|
742b3a0775 | ||
|
|
71ce668e3c | ||
|
|
561a2fc9b2 | ||
|
|
08611349aa | ||
|
|
bbf2e605b5 |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-07-31T00:35:57.879961400Z">
|
||||
<DropdownSelection timestamp="2025-11-07T10:55:54.677927600Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=2FD2511004054749" />
|
||||
<DeviceId pluginId="Default" identifier="serial=192.168.1.56:5555;connection=2687c52a" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="gradleJvm" value="azul-17" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "adfceb8d-2c35-41bf-82d9-40b87e10524d",
|
||||
"name": "档口机",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "751386"
|
||||
},
|
||||
"item": [{
|
||||
"name": "device获取token",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"
|
||||
raw": "http://device.shuziweidao.com:8889/sys/getEquipmentToken?qrcodeId=3ea47dc0-3cf0-3c2f-909c-265a9a65572e",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"device",
|
||||
"shuziweidao",
|
||||
"com"
|
||||
],
|
||||
"port": "8889",
|
||||
"path": [
|
||||
"sys",
|
||||
"getEquipmentToken"
|
||||
],
|
||||
"query": [{
|
||||
"key": "qrcodeId",
|
||||
"value": "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}, {
|
||||
"name": "获取配置信息",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [{
|
||||
"key": "X-Access-Token",
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NTM4NDIzNDEsInVzZXJuYW1lIjoiYWRtaW4ifQ.gcKT7BeMWNve8NydRmTTrdXW8wdS7W_Ot9kVdtllyuc",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"
|
||||
raw": "http://device.shuziweidao.com:8889/equipment/stEquipment/queryByEquipmentCode?equipmentCode=3ea47dc0-3cf0-3c2f-909c-265a9a65572e",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"device",
|
||||
"shuziweidao",
|
||||
"com"
|
||||
],
|
||||
"port": "8889",
|
||||
"path": [
|
||||
"equipment",
|
||||
"stEquipment",
|
||||
"queryByEquipmentCode"
|
||||
],
|
||||
"query": [{
|
||||
"key": "equipmentCode",
|
||||
"value": "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}, {
|
||||
"name": "获取业务服务器token",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"
|
||||
raw": "http://192.168.1.207/gateway/local/sys/getEquipmentToken?qrcodeId=3ea47dc0-3cf0-3c2f-909c-265a9a65572e",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"192",
|
||||
"168",
|
||||
"1",
|
||||
"207"
|
||||
],
|
||||
"path": [
|
||||
"gateway",
|
||||
"local",
|
||||
"sys",
|
||||
"getEquipmentToken"
|
||||
],
|
||||
"query": [{
|
||||
"key": "qrcodeId",
|
||||
"value": "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}, {
|
||||
"name": "获取人脸数据",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [{
|
||||
"key": "X-Access-Token",
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiIyOCIsInR5cGUiOiI0IiwidXNlcklkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMSJ9.Jy7Dc2k5FlqUAmGReRn-uEg3Du5gtlyUhXKUMZLlO-E",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "http://192.168.1.207/gateway/local/stapi/cquser/getUserFaceCache/v2?pageIndex=0",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"192",
|
||||
"168",
|
||||
"1",
|
||||
"207"
|
||||
],
|
||||
"path": [
|
||||
"gateway",
|
||||
"local",
|
||||
"stapi",
|
||||
"cquser",
|
||||
"getUserFaceCache",
|
||||
"v2"
|
||||
],
|
||||
"query": [{
|
||||
"key": "pageIndex",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}, {
|
||||
"name": "获取档口菜品信息",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [{
|
||||
"key": "X-Access-Token",
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiIyOCIsInR5cGUiOiI0IiwidXNlcklkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMSJ9.Jy7Dc2k5FlqUAmGReRn-uEg3Du5gtlyUhXKUMZLlO-E",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"
|
||||
raw": "http://192.168.1.207/gateway/local/zhstapi/zhst/getRestInfoFoodsByType/stall?restId=1678234139391512577&type=0&foodName=西红柿",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"192",
|
||||
"168",
|
||||
"1",
|
||||
"207"
|
||||
],
|
||||
"path": [
|
||||
"gateway",
|
||||
"local",
|
||||
"zhstapi",
|
||||
"zhst",
|
||||
"getRestInfoFoodsByType",
|
||||
"stall"
|
||||
],
|
||||
"query": [{
|
||||
"key": "restId",
|
||||
"value": "1678234139391512577"
|
||||
}, {
|
||||
"key": "type",
|
||||
"value": "0",
|
||||
"description": "0全部 1餐次"
|
||||
}, {
|
||||
"key": "foodName",
|
||||
"value": "西红柿"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}, {
|
||||
"name": "通过用户信息获取就餐数据",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [{
|
||||
"key": "X-Access-Token",
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYW50ZWVuSWQiOiIyOCIsInR5cGUiOiI0IiwidXNlcklkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMSJ9.Jy7Dc2k5FlqUAmGReRn-uEg3Du5gtlyUhXKUMZLlO-E",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"
|
||||
raw": "http://192.168.1.207/gateway/local/zhstapi/zhst/getUserNutritionData/face?restId=1678234139391512577&userId=4958698b725b4e89a514fa8552aa5fa3&foodId=1701417944860119041",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"192",
|
||||
"168",
|
||||
"1",
|
||||
"207"
|
||||
],
|
||||
"path": [
|
||||
"gateway",
|
||||
"local",
|
||||
"zhstapi",
|
||||
"zhst",
|
||||
"getUserNutritionData",
|
||||
"face"
|
||||
],
|
||||
"query": [{
|
||||
"key": "restId",
|
||||
"value": "1678234139391512577"
|
||||
}, {
|
||||
"key": "userId",
|
||||
"value": "4958698b725b4e89a514fa8552aa5fa3"
|
||||
}, {
|
||||
"key": "foodId",
|
||||
"value": "1701417944860119041"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}, {
|
||||
"name": "提交就餐数据",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"
|
||||
raw": "http://192.168.1.207/gateway/local/zhstapi/zhst/userEatFood/doubleBalance/face/stall?restId=1678234139391512577&userId=4958698b725b4e89a514fa8552aa5fa3&foodId=1701417944860119041&faceTime&faceEndTime&eatWeight&foodWeight&deviceId",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"192",
|
||||
"168",
|
||||
"1",
|
||||
"207"
|
||||
],
|
||||
"path": [
|
||||
"gateway",
|
||||
"local",
|
||||
"zhstapi",
|
||||
"zhst",
|
||||
"userEatFood",
|
||||
"doubleBalance",
|
||||
"face",
|
||||
"stall"
|
||||
],
|
||||
"body": [{
|
||||
"appVersion": "1",
|
||||
"deviceId": "abe60247-99d1-3c2a-9f10-c0b76f4b3f3b",
|
||||
"eatWeight": 0.278,
|
||||
"faceEndTime": 1754385701611,
|
||||
"faceTime": 1754385694475,
|
||||
"foodId": "1701417944860119041",
|
||||
"foodWeight": 0.278,
|
||||
"restId": "1678234139391512577",
|
||||
"userId": "1937692681894932481"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
### 灯规则
|
||||
默认 红
|
||||
识别到菜 黄
|
||||
识别到人 绿
|
||||
@@ -1,9 +1,12 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
id("com.google.devtools.ksp")
|
||||
// alias(libs.plugins.android.application)
|
||||
// alias(libs.plugins.kotlin.android)
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("com.google.devtools.ksp")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -29,6 +32,7 @@ android {
|
||||
ndk {
|
||||
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
|
||||
}
|
||||
setProperty("archivesBaseName", "档口双屏_${versionName}")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -53,14 +57,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// implementation(
|
||||
// fileTree(
|
||||
// mapOf(
|
||||
// "dir" to "libs",
|
||||
// "include" to listOf("*.aar", "*.jar")
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
@@ -104,4 +101,19 @@ dependencies {
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
// 图片加载
|
||||
implementation(libs.glide)
|
||||
|
||||
implementation(libs.core)
|
||||
implementation(libs.android.core)
|
||||
|
||||
implementation(libs.pytorch.android)
|
||||
implementation(libs.pytorch.android.torchvision)
|
||||
|
||||
val objectboxVersion = "5.0.1"
|
||||
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
|
||||
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
|
||||
|
||||
implementation(libs.androidx.recyclerview)
|
||||
|
||||
}
|
||||
|
||||
apply(plugin = "io.objectbox")
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "1:594331511073099531",
|
||||
"lastPropertyId": "4:1528837175750321569",
|
||||
"name": "Food",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:2317727855243736226",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:340951913928211738",
|
||||
"name": "name",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "3:5484846939472684412",
|
||||
"name": "foodIdx",
|
||||
"type": 5
|
||||
},
|
||||
{
|
||||
"id": "4:1528837175750321569",
|
||||
"name": "foodVector",
|
||||
"indexId": "1:6010838397086628487",
|
||||
"type": 28,
|
||||
"flags": 8
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "1:594331511073099531",
|
||||
"lastIndexId": "1:6010838397086628487",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 5,
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:name=".MyApp"
|
||||
@@ -25,8 +26,18 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.DualScreen"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<!-- 注册BootReceiver,监听开机完成广播 -->
|
||||
<receiver
|
||||
android:name=".receiver.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<activity
|
||||
android:name=".activity.InitActivity"
|
||||
android:exported="true">
|
||||
@@ -40,6 +51,22 @@
|
||||
android:name=".activity.MainActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.FoodCollectionActivity"
|
||||
android:exported="true">
|
||||
|
||||
</activity>
|
||||
<activity android:name="com.sw.dualscreen.activity.CollectedDataActivity" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
After Width: | Height: | Size: 515 KiB |
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"class_names": [
|
||||
"回锅肉",
|
||||
"手撕包菜",
|
||||
"炒细面",
|
||||
"红烧肉",
|
||||
"肉沫酸豆角",
|
||||
"蒜苔炒肉",
|
||||
"西红柿鸡蛋",
|
||||
"豆角茄子",
|
||||
"麻婆豆腐",
|
||||
"麻辣小面"
|
||||
],
|
||||
"class_to_idx": {
|
||||
"回锅肉": 0,
|
||||
"手撕包菜": 1,
|
||||
"炒细面": 2,
|
||||
"红烧肉": 3,
|
||||
"肉沫酸豆角": 4,
|
||||
"蒜苔炒肉": 5,
|
||||
"西红柿鸡蛋": 6,
|
||||
"豆角茄子": 7,
|
||||
"麻婆豆腐": 8,
|
||||
"麻辣小面": 9
|
||||
},
|
||||
"idx_to_class": {
|
||||
"0": "回锅肉",
|
||||
"1": "手撕包菜",
|
||||
"2": "炒细面",
|
||||
"3": "红烧肉",
|
||||
"4": "肉沫酸豆角",
|
||||
"5": "蒜苔炒肉",
|
||||
"6": "西红柿鸡蛋",
|
||||
"7": "豆角茄子",
|
||||
"8": "麻婆豆腐",
|
||||
"9": "麻辣小面"
|
||||
},
|
||||
"embedding_dim": 512,
|
||||
"total_images": 500,
|
||||
"build_time": "2025-10-16T17:51:06.285784"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
|
||||
@@ -7,6 +7,11 @@ object GlobalData {
|
||||
// var globalEquipmentCode: String = "202501171634"
|
||||
var restId: String = ""
|
||||
|
||||
/**
|
||||
* 设备id
|
||||
*/
|
||||
var deviceId: String = ""
|
||||
|
||||
/**
|
||||
* app版本号
|
||||
*/
|
||||
@@ -34,7 +39,7 @@ object GlobalData {
|
||||
|
||||
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
|
||||
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
|
||||
var activeKey = "085F-118G-Q3AB-1WVJ"
|
||||
var activeKey = "085F-118G-Q3LH-HPGZ"//2号楼"085F-118G-Q3AB-1WVJ" 5号楼"085F-118G-Q3LH-HPGZ"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,4 +60,5 @@ object GlobalKey {
|
||||
const val KEY_TOKEN = "tokenKey"
|
||||
const val KEY_FIRST_RUN = "firstRun"
|
||||
const val KEY_USER_INFO = "userInfoKey"
|
||||
const val KEY_PICKUP_MODE = "pickupMode"
|
||||
}
|
||||
@@ -1,15 +1,27 @@
|
||||
package com.sw.dualscreen
|
||||
|
||||
import com.sw.dualscreen.objbox.ObjectBox
|
||||
import com.sw.dualscreen.utils.CrashHandler
|
||||
import com.sw.plate.App
|
||||
import com.sw.plate.utils.AppUtil
|
||||
import timber.log.Timber
|
||||
|
||||
class MyApp : App() {
|
||||
companion object {
|
||||
const val DEBUG: Boolean = true
|
||||
var instance: MyApp?=null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
Timber.plant(Timber.DebugTree())
|
||||
var deviceId = AppUtil.getUDID(this)
|
||||
Timber.d("UDID = ${AppUtil.getUDID(this)}")
|
||||
deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e"
|
||||
GlobalData.deviceId = deviceId
|
||||
ObjectBox.init(this)
|
||||
// 初始化崩溃处理器
|
||||
CrashHandler.init(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.sw.dualscreen.activity
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.sw.dualscreen.R
|
||||
import com.sw.dualscreen.model.IActivityResult
|
||||
import com.sw.dualscreen.view.CustomDialog
|
||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||
protected lateinit var binding: VB
|
||||
protected lateinit var context: Context
|
||||
private var mDialogWaiting: CustomDialog? = null
|
||||
private lateinit var viewModel: BaseViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
context = this
|
||||
|
||||
//保持亮屏
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
binding = inflateViewBinding()
|
||||
setContentView(binding.root)
|
||||
viewModel = getViewModel()
|
||||
initialize()
|
||||
registerDataChange()
|
||||
}
|
||||
|
||||
abstract fun getViewModel(): BaseViewModel
|
||||
|
||||
protected abstract fun inflateViewBinding(): VB
|
||||
|
||||
open fun initialize() {
|
||||
|
||||
}
|
||||
|
||||
open fun registerDataChange() {
|
||||
lifecycleScope.launch {
|
||||
viewModel.showLoading.collect {
|
||||
Timber.d("registerDataChange 用户 showLoading = $it")
|
||||
if (it) {
|
||||
showWaitingDialog("")
|
||||
} else {
|
||||
hideWaitingDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示等待提示框
|
||||
*/
|
||||
fun showWaitingDialog(tip: String?): Dialog? {
|
||||
hideWaitingDialog()
|
||||
val view = View.inflate(this, R.layout.dialog_waiting, null)
|
||||
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
|
||||
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
|
||||
mDialogWaiting!!.show()
|
||||
mDialogWaiting!!.setCancelable(true)
|
||||
return mDialogWaiting
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏等待提示框
|
||||
*/
|
||||
fun hideWaitingDialog() {
|
||||
mDialogWaiting?.dismiss()
|
||||
mDialogWaiting = null
|
||||
}
|
||||
|
||||
private var launchPermissionCallback: IActivityResult.RequestPermissionCallback? = null
|
||||
|
||||
fun requestSinglePermissionResult(
|
||||
permission: String,
|
||||
permissionCallback: IActivityResult.RequestPermissionCallback?
|
||||
) {
|
||||
this.launchPermissionCallback = permissionCallback
|
||||
permissionLauncher.launch(permission)
|
||||
}
|
||||
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { result: Boolean ->
|
||||
launchPermissionCallback?.callback(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.sw.dualscreen.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.sw.dualscreen.R
|
||||
import com.sw.dualscreen.adapter.CollectedFoodAdapter
|
||||
import com.sw.dualscreen.databinding.ActivityCollectedDataBinding
|
||||
import com.sw.dualscreen.databinding.LayoutEmptySearchBinding
|
||||
import com.sw.dualscreen.dialog.WarnDialog
|
||||
import com.sw.dualscreen.ext.addOnActionSearchListener
|
||||
import com.sw.dualscreen.ext.hideKeyboard
|
||||
import com.sw.dualscreen.objbox.CollectedFoodBean
|
||||
import com.sw.dualscreen.objbox.Food
|
||||
import com.sw.dualscreen.objbox.ObjectBox
|
||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
||||
import com.sw.plate.utils.ToastUtils
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import kotlin.getValue
|
||||
|
||||
class CollectedDataActivity : BaseActivity<ActivityCollectedDataBinding>() {
|
||||
private val viewModel by viewModels<UserViewModel>()
|
||||
override fun getViewModel(): BaseViewModel {
|
||||
return viewModel
|
||||
}
|
||||
|
||||
override fun inflateViewBinding(): ActivityCollectedDataBinding {
|
||||
return ActivityCollectedDataBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
private val list: MutableList<CollectedFoodBean> = mutableListOf()
|
||||
private val adapter by lazy {
|
||||
CollectedFoodAdapter(list).apply {
|
||||
isStateViewEnable = true
|
||||
addOnItemChildClickListener(R.id.ivDeleteFood) { _, _, position ->
|
||||
deleteGoods(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
super.initialize()
|
||||
binding.rvFoodList.let {
|
||||
it.layoutManager = LinearLayoutManager(this)
|
||||
it.adapter = adapter
|
||||
}
|
||||
binding.ivBack.setOnClickListener { finish() }
|
||||
binding.ivGoodsSearch.setOnClickListener {
|
||||
val searchName = binding.etInputGoods.text.toString().trim()
|
||||
if (searchName.isBlank()) {
|
||||
ToastUtils.showToast("请输入物品名称")
|
||||
return@setOnClickListener
|
||||
}
|
||||
getCollectGoods(searchName)
|
||||
}
|
||||
binding.etInputGoods.let { v ->
|
||||
v.addOnActionSearchListener {
|
||||
val searchName = binding.etInputGoods.text.toString().trim()
|
||||
if (searchName.isBlank()) {
|
||||
ToastUtils.showToast("请输入物品名称")
|
||||
return@addOnActionSearchListener
|
||||
}
|
||||
getCollectGoods(searchName)
|
||||
}
|
||||
v.addTextChangedListener {
|
||||
if (it.isNullOrBlank()) {
|
||||
getCollectGoods()
|
||||
}
|
||||
}
|
||||
}
|
||||
getCollectGoods()
|
||||
}
|
||||
|
||||
|
||||
private var box: Box<Food>? = null
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun getCollectGoods(searchName:String?=null) {
|
||||
if (box == null) {
|
||||
box = ObjectBox.boxStore.boxFor(Food::class)
|
||||
}
|
||||
list.clear()
|
||||
var queryList = box?.all?.distinctBy { it.name }
|
||||
if (searchName.isNullOrBlank().not()) {
|
||||
queryList = queryList?.filter { it.name?.contains(searchName) == true }
|
||||
}
|
||||
queryList?.forEach {
|
||||
list.add(CollectedFoodBean(foodName = it.name))
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
if (list.isEmpty()) {
|
||||
loadEmptyView()
|
||||
}
|
||||
binding.root.hideKeyboard()
|
||||
}
|
||||
|
||||
private var emptyBinding: LayoutEmptySearchBinding? = null
|
||||
private fun loadEmptyView() {
|
||||
if (emptyBinding == null) {
|
||||
emptyBinding = LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvFoodList, false)
|
||||
}
|
||||
emptyBinding?.root?.let { layout ->
|
||||
layout.setOnClickListener { layout.hideKeyboard() }
|
||||
adapter.stateView = layout
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteGoods(position: Int) {
|
||||
WarnDialog(
|
||||
context = context,
|
||||
content = "请确认是否删除菜品:${list[position].foodName}?",
|
||||
confirmBlock = {
|
||||
Thread {
|
||||
runOnUiThread {
|
||||
showWaitingDialog("加载中……")
|
||||
}
|
||||
val name = list[position].foodName
|
||||
val filterIdList = box?.all?.filter { it.name == name }?.map { it.id }
|
||||
box?.removeByIds(filterIdList)
|
||||
runOnUiThread {
|
||||
hideWaitingDialog()
|
||||
adapter.removeAt(position)
|
||||
ToastUtils.showToast("删除成功")
|
||||
if (list.isEmpty()) {
|
||||
loadEmptyView()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}).show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package com.sw.dualscreen.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.example.utils.FloatBase64Utils
|
||||
import com.sw.dualscreen.R
|
||||
import com.sw.dualscreen.adapter.FoodCollectionAdapter
|
||||
import com.sw.dualscreen.adapter.GenericItemAdapter
|
||||
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
|
||||
import com.sw.dualscreen.adapter.dpToPx
|
||||
import com.sw.dualscreen.databinding.ActivityFoodCollectionBinding
|
||||
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
|
||||
import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding
|
||||
import com.sw.dualscreen.ext.clickWithDebounce
|
||||
import com.sw.dualscreen.ext.dp
|
||||
import com.sw.dualscreen.model.response.FoodInfo
|
||||
import com.sw.dualscreen.objbox.Food
|
||||
import com.sw.dualscreen.objbox.FoodCollectionBean
|
||||
import com.sw.dualscreen.objbox.FoodModule
|
||||
import com.sw.dualscreen.objbox.ObjectBox
|
||||
import com.sw.dualscreen.utils.BitmapCropper
|
||||
import com.sw.dualscreen.utils.BitmapSaver
|
||||
import com.sw.dualscreen.utils.CameraHelper
|
||||
import com.sw.dualscreen.utils.CameraUtils
|
||||
import com.sw.dualscreen.utils.Debouncer
|
||||
import com.sw.dualscreen.utils.ImageUtil
|
||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
||||
import com.sw.plate.utils.ToastUtils
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pytorch.IValue
|
||||
import org.pytorch.Module
|
||||
import org.pytorch.torchvision.TensorImageUtils
|
||||
import timber.log.Timber
|
||||
import kotlin.math.max
|
||||
|
||||
class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
|
||||
companion object {
|
||||
val MAX_COUNT = 100
|
||||
}
|
||||
private val viewModel by viewModels<UserViewModel>()
|
||||
private var selectedFoodId: String? = ""
|
||||
private var selectedFoodName: String? = ""
|
||||
|
||||
private var box: Box<Food>? = null
|
||||
private val foodCollectionList: MutableList<FoodCollectionBean> = mutableListOf()
|
||||
|
||||
// private lateinit var cameraHelper: CameraHelper
|
||||
|
||||
private lateinit var foodAdapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
|
||||
private val foodList = mutableListOf<FoodInfo>() // 适配器内部维护的数据列表
|
||||
private val debouncer = Debouncer(2000)
|
||||
private lateinit var previewView: PreviewView
|
||||
|
||||
private val cameraUtils: CameraUtils by lazy {
|
||||
CameraUtils(this)
|
||||
}
|
||||
|
||||
private val collectionAdapter: FoodCollectionAdapter by lazy {
|
||||
FoodCollectionAdapter(foodCollectionList).apply {
|
||||
// setOnItemClickListener { _, _, position ->
|
||||
// if (list[position].isShowCamera) {
|
||||
//// takePhoto()
|
||||
// cameraHelper.openCamera()
|
||||
// }
|
||||
// }
|
||||
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
|
||||
// list.removeAt(position)
|
||||
// collectionAdapter.notifyItemRemoved(position)
|
||||
// collectionAdapter.notifyItemRangeChanged(position, list.size)
|
||||
foodCollectionList[position].let {
|
||||
it.bitmap = null
|
||||
it.isShowCamera = true
|
||||
it.isFinish = false
|
||||
}
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewModel(): BaseViewModel {
|
||||
return viewModel
|
||||
}
|
||||
|
||||
override fun inflateViewBinding(): ActivityFoodCollectionBinding {
|
||||
return ActivityFoodCollectionBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
private val cameraCallback: (Uri) -> Unit = { uri ->
|
||||
val index = foodCollectionList.indexOfFirst { it.bitmap == null }
|
||||
if (index == -1) {
|
||||
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
|
||||
rerurn@ cameraCallback
|
||||
}
|
||||
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
|
||||
// val cropBitmap = BitmapCropper.cropCenter(
|
||||
// original = bitmap,
|
||||
// targetWidth = 1300, targetHeight = 900,
|
||||
// //offsetX = 30, offsetY = 100
|
||||
// )
|
||||
val file = BitmapSaver.saveToAppFilesDir(
|
||||
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||||
)
|
||||
Timber.d("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
||||
|
||||
foodCollectionList[index].let {
|
||||
it.bitmap = bitmap
|
||||
it.isShowCamera = false
|
||||
it.imageUri = file?.toUri()
|
||||
}
|
||||
collectionAdapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun takePhoto() {
|
||||
val count = foodCollectionList.count { it.bitmap != null }
|
||||
if (count >= MAX_COUNT) {
|
||||
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
|
||||
return
|
||||
}
|
||||
cameraUtils.takePhoto(cameraCallback)
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
cameraUtils.initCamera()
|
||||
val previewBinding =
|
||||
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
|
||||
previewView = previewBinding.previewView.also {
|
||||
it.updateLayoutParams {
|
||||
width = 180.dp
|
||||
height = 180.dp
|
||||
}
|
||||
}
|
||||
cameraUtils.setPreviewController(previewView)
|
||||
// 初始化 CameraHelper
|
||||
// cameraHelper = CameraHelper(
|
||||
// context = this,
|
||||
// caller = this,
|
||||
// authority = "${packageName}.fileprovider"
|
||||
// ) { uri, path ->
|
||||
// //if (foodCollectionList.size < 6) {
|
||||
// // val insertIndex = if (foodCollectionList.isEmpty()) 0 else foodCollectionList.size - 1
|
||||
// // foodCollectionList.add(insertIndex, FoodCollectionBean(imageUri = uri))
|
||||
// //} else {
|
||||
// // foodCollectionList[foodCollectionList.size - 1] = FoodCollectionBean(imageUri = uri)
|
||||
// //}
|
||||
// //collectionAdapter.notifyDataSetChanged()
|
||||
// cameraCallback(uri)
|
||||
// }
|
||||
|
||||
binding.ivBack.setOnClickListener {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
repeat(MAX_COUNT) {
|
||||
foodCollectionList.add(FoodCollectionBean(isShowCamera = true))
|
||||
}
|
||||
binding.rvFoodCollection.let {
|
||||
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
|
||||
it.adapter = collectionAdapter
|
||||
}
|
||||
|
||||
binding.btnFoodSearch.setOnClickListener {
|
||||
searchInfo()
|
||||
}
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
if (selectedFoodName.isNullOrBlank()) {
|
||||
Toast.makeText(this, "请选择菜品名称", Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
if (foodCollectionList.size <= 1) {
|
||||
Toast.makeText(this, "请拍摄菜品照片", Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
vectorThread()
|
||||
}
|
||||
|
||||
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
searchInfo()
|
||||
val imm =
|
||||
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnCollectedGoods.setOnClickListener {
|
||||
startActivity(Intent(this, CollectedDataActivity::class.java))
|
||||
}
|
||||
binding.btnTakePhoto.clickWithDebounce {
|
||||
binding.btnTakePhoto.text = "拍照"
|
||||
val count = foodCollectionList.count { it.bitmap != null }
|
||||
if (count >= MAX_COUNT) {
|
||||
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
|
||||
return@clickWithDebounce
|
||||
}
|
||||
// cameraHelper.openCamera()
|
||||
takePhoto()
|
||||
}
|
||||
binding.btnClearData.setOnClickListener { clearData() }
|
||||
foodAdapter = createAdapter()
|
||||
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
|
||||
// 添加间距装饰(12dp)
|
||||
binding.recyclerview.addItemDecoration(
|
||||
GridSpacingItemDecoration(
|
||||
spanCount = 2,
|
||||
spacing = dpToPx(30),
|
||||
includeEdge = false // 包含边缘间距
|
||||
)
|
||||
)
|
||||
binding.recyclerview.adapter = foodAdapter
|
||||
}
|
||||
|
||||
private fun vectorThread() {
|
||||
//Loading.show(this)
|
||||
showWaitingDialog("加载中……")
|
||||
Thread {
|
||||
foodCollectionList
|
||||
.filter { it.bitmap != null }
|
||||
.forEachIndexed { index, it ->
|
||||
image2VectorTask(it, index)
|
||||
}
|
||||
runOnUiThread {
|
||||
window.decorView.postDelayed({
|
||||
//Loading.dismiss()
|
||||
hideWaitingDialog()
|
||||
}, 1000)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun initBox() {
|
||||
if (box == null) {
|
||||
box = ObjectBox.boxStore.boxFor(Food::class)
|
||||
}
|
||||
}
|
||||
|
||||
private fun image2VectorTask(item: FoodCollectionBean, position: Int) {
|
||||
initBox()
|
||||
val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!)
|
||||
|
||||
val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector)
|
||||
Timber.tag("mzf1").e(base64Str)
|
||||
// viewModel.postImageData(
|
||||
// context,
|
||||
// foodId = selectedFoodId.toString(),
|
||||
// foodName = selectedFoodName.toString(),
|
||||
// foodVector = base64Str,
|
||||
// uri = item.imageUri!!
|
||||
// )
|
||||
box?.put(Food(name = checkedItem!!.foodName, foodIdx = 0, foodVector = imageVector))
|
||||
foodCollectionList[position].let {
|
||||
it.imageVector = imageVector
|
||||
it.isFinish = true
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
collectionAdapter.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun searchInfo() {
|
||||
debouncer.debounce {
|
||||
viewModel.searchByFoodName(binding.editFoodName.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerDataChange() {
|
||||
super.registerDataChange()
|
||||
lifecycleScope.launch {
|
||||
viewModel.searchFoodInfoList.collect {
|
||||
// foodList.clear()
|
||||
// foodList.addAll(it)
|
||||
foodAdapter.updateData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var checkedItem: FoodInfo? = null
|
||||
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
|
||||
return GenericItemAdapter(
|
||||
items = emptyList(),
|
||||
bindingInflater = ItemSearchFoodInfoBinding::inflate,
|
||||
bindCallback = { item, position ->
|
||||
this.tvName.text = item.foodName
|
||||
if (item.id != checkedItem?.id) {
|
||||
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
|
||||
this.tvName.setTextColor(resources.getColor(R.color.search_normal))
|
||||
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_normal)
|
||||
} else {
|
||||
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
|
||||
this.tvName.setTextColor(resources.getColor(R.color.search_checked))
|
||||
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_checked)
|
||||
}
|
||||
|
||||
this.llRoot.setOnClickListener {
|
||||
Timber.d("itemClick ${item.foodName}, position = $position")
|
||||
checkedItem = item
|
||||
// viewModel.updateCurrentItem(item)
|
||||
// itemClickCallback(item)
|
||||
selectedFoodName = item.foodName
|
||||
selectedFoodId = item.id
|
||||
|
||||
foodAdapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var clickIndex = -1
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun clearData() {
|
||||
foodCollectionList.forEach {
|
||||
it.bitmap = null
|
||||
it.isShowCamera = true
|
||||
it.isFinish = false
|
||||
}
|
||||
collectionAdapter.notifyDataSetChanged()
|
||||
|
||||
clickIndex = -1
|
||||
binding.editFoodName.setText("")
|
||||
// foodList.clear()
|
||||
// foodAdapter.updateData(mutableListOf())
|
||||
// foodAdapter.notifyDataSetChanged()
|
||||
//loadEmptyView()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
cameraUtils.bind()
|
||||
binding.llCameraFlag.run {
|
||||
visibility = View.VISIBLE
|
||||
postDelayed({
|
||||
visibility = View.GONE
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
cameraUtils.unbind()
|
||||
binding.llCameraFlag.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,35 +5,48 @@ import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.sw.dualscreen.GlobalData
|
||||
import com.sw.dualscreen.databinding.ActivityInitBinding
|
||||
import com.sw.dualscreen.utils.QRCodeUtil
|
||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||
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
|
||||
class InitActivity : BaseActivity<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)
|
||||
override fun getViewModel(): BaseViewModel {
|
||||
return viewModel
|
||||
}
|
||||
|
||||
override fun inflateViewBinding(): ActivityInitBinding {
|
||||
return ActivityInitBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
super.initialize()
|
||||
val isSuccess = viewModel.checkEquipmentInfo()
|
||||
if (isSuccess) {
|
||||
goMainActivity()
|
||||
return
|
||||
}
|
||||
binding.tvDeviceId.text = deviceId
|
||||
binding.btConfirm.setOnClickListener {
|
||||
binding.imgQr.setImageBitmap(
|
||||
QRCodeUtil.generateQRCode(
|
||||
content = GlobalData.deviceId,
|
||||
size = 200
|
||||
)
|
||||
)
|
||||
binding.initButton.setOnClickListener {
|
||||
viewModel.getDeviceToken()
|
||||
}
|
||||
registerDataChange()
|
||||
}
|
||||
|
||||
private fun registerDataChange() {
|
||||
override fun registerDataChange() {
|
||||
super.registerDataChange()
|
||||
lifecycleScope.launch {
|
||||
viewModel.deviceInfoResult.drop(1).collect {
|
||||
if (it != true) return@collect
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package com.sw.dualscreen.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Typeface
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.os.Bundle
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageCapture
|
||||
@@ -22,6 +23,8 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.sw.dualscreen.GlobalData
|
||||
import com.sw.dualscreen.GlobalKey
|
||||
import com.sw.dualscreen.R
|
||||
import com.sw.dualscreen.adapter.GenericItemAdapter
|
||||
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
|
||||
@@ -29,53 +32,81 @@ import com.sw.dualscreen.adapter.dpToPx
|
||||
import com.sw.dualscreen.databinding.ActivityMainBinding
|
||||
import com.sw.dualscreen.databinding.ItemFoodInfoBinding
|
||||
import com.sw.dualscreen.ext.dp
|
||||
import com.sw.dualscreen.ext.format2String
|
||||
import com.sw.dualscreen.model.response.FoodInfo
|
||||
import com.sw.dualscreen.objbox.FoodModule
|
||||
import com.sw.dualscreen.objbox.FoodModule.IdNameScore
|
||||
import com.sw.dualscreen.presentation.SecondaryScreenPresentation
|
||||
import com.sw.dualscreen.sdk.SensorScaleUtils
|
||||
import com.sw.dualscreen.utils.AssetsTool
|
||||
import com.sw.dualscreen.utils.BitmapCropper
|
||||
import com.sw.dualscreen.utils.BitmapSaver
|
||||
import com.sw.dualscreen.utils.Debouncer
|
||||
import com.sw.dualscreen.utils.GlideUtils
|
||||
import com.sw.dualscreen.utils.GsonUtils
|
||||
import com.sw.dualscreen.utils.ImageUtil
|
||||
import com.sw.dualscreen.utils.SPUtil
|
||||
import com.sw.dualscreen.view.CustomBottomSheetDialog
|
||||
import com.sw.dualscreen.viewmodel.BaseViewModel
|
||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
||||
import com.sw.plate.utils.AppUtil
|
||||
import com.sw.plate.utils.LightManager
|
||||
import com.sw.plate.utils.ToastUtils
|
||||
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
/**
|
||||
* 服务员显示界面
|
||||
*/
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
private val viewModel by viewModels<UserViewModel>()
|
||||
private var imageCapture: ImageCapture? = null
|
||||
private var isAnalyzing = true // 控制是否进行图像分析
|
||||
private lateinit var presentation: SecondaryScreenPresentation
|
||||
private var presentation: SecondaryScreenPresentation? = null
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private lateinit var adapter: GenericItemAdapter<FoodInfo, ItemFoodInfoBinding>
|
||||
private var checkedItem: FoodInfo? = null
|
||||
private var imageAnalysis: ImageAnalysis? = null
|
||||
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
|
||||
private var canIdentify: Boolean = true
|
||||
private var canIdentify: Boolean = false
|
||||
private var bottomSheetDialog: CustomBottomSheetDialog? = null
|
||||
private var lastWeight: Double = 0.0
|
||||
private var lastPhotoUri: Uri? = null // 最后拍照的图片
|
||||
private var debouncer = Debouncer(2000)
|
||||
private var isRecognitionFood = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.main)
|
||||
override fun getViewModel(): BaseViewModel {
|
||||
return viewModel
|
||||
}
|
||||
|
||||
Timber.d("UDID = ${AppUtil.getUDID(this)}")
|
||||
override fun inflateViewBinding(): ActivityMainBinding {
|
||||
return ActivityMainBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
super.initialize()
|
||||
initView()
|
||||
setupSecondaryDisplay()
|
||||
setupCamera()
|
||||
initData()
|
||||
FoodModule.init(this)
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
viewModel.getEquipmentToken()
|
||||
presentation.step1()
|
||||
presentation?.step1()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewModel.activeEngine()
|
||||
binding.foodDisplayLayout.setOnClickListener {
|
||||
val intent = Intent(this, FoodCollectionActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
|
||||
@@ -83,19 +114,22 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
binding.previewView.clipToOutline = true
|
||||
binding.tvToSearch.setOnClickListener {
|
||||
val bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) {
|
||||
bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) {
|
||||
it.photoUri = null
|
||||
updateCurrentFood(it)
|
||||
}
|
||||
bottomSheetDialog.show(supportFragmentManager, "CustomBottomSheetDialog")
|
||||
bottomSheetDialog!!.show(supportFragmentManager, "CustomBottomSheetDialog")
|
||||
}
|
||||
|
||||
val spinnerAdapter = ArrayAdapter<String>(
|
||||
this,
|
||||
R.layout.item_for_custom_spinner,
|
||||
arrayOf("即放即取", "持续取餐")
|
||||
arrayOf("即放即取", "余量取餐")
|
||||
)
|
||||
binding.spinner.adapter = spinnerAdapter
|
||||
binding.spinner.setSelection(0)
|
||||
val mode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
|
||||
binding.spinner.setSelection(mode ?: 0)
|
||||
presentation?.updateMealPickupMode(mode ?: 0)
|
||||
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
@@ -103,12 +137,15 @@ class MainActivity : AppCompatActivity() {
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, position)
|
||||
updateCurrentFood(null)
|
||||
isRecognitionFood = true
|
||||
presentation?.updateMealPickupMode(position)
|
||||
|
||||
SensorScaleUtils.zero()
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
adapter = createAdapter()
|
||||
binding.recyclerview.layoutManager = GridLayoutManager(this, 2)
|
||||
@@ -122,22 +159,32 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
binding.recyclerview.adapter = adapter
|
||||
binding.tvFoodName.setOnClickListener {
|
||||
if (presentation.currentStep == 2) {
|
||||
presentation.step3()
|
||||
} else {
|
||||
presentation.step1()
|
||||
SensorScaleUtils.zero()
|
||||
}
|
||||
binding.tvRescan.setOnClickListener {
|
||||
debouncer.debounce { recognizeFood() }
|
||||
}
|
||||
registerDataChange()
|
||||
}
|
||||
|
||||
fun updateCurrentFood(foodInfo: FoodInfo?) {
|
||||
presentation.updateFood(foodInfo)
|
||||
if (foodInfo != null) {
|
||||
presentation?.updateFood(foodInfo)
|
||||
binding.tvFoodName.text = foodInfo.foodName
|
||||
binding.previewView.visibility = View.GONE
|
||||
binding.ivImg.visibility = View.VISIBLE
|
||||
val imgUrl = foodInfo.imgUrl
|
||||
if (TextUtils.isEmpty(imgUrl)) {
|
||||
takePhoto { imgUri ->
|
||||
GlideUtils.loadRoundCornerImage(this, imgUri, binding.ivImg, 12)
|
||||
presentation?.updateFood(foodInfo.copy(imgUrl = imgUri.toString()))
|
||||
}
|
||||
} else {
|
||||
if (foodInfo.photoUri != null) {
|
||||
GlideUtils.loadRoundCornerImage(this, foodInfo.photoUri, binding.ivImg, 12)
|
||||
} else {
|
||||
GlideUtils.loadRoundCornerImage(this, foodInfo.imgUrl, binding.ivImg, 12)
|
||||
}
|
||||
}
|
||||
pauseAnalysis()
|
||||
} else {
|
||||
binding.tvFoodName.text = "-"
|
||||
@@ -148,23 +195,106 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerDataChange() {
|
||||
override fun registerDataChange() {
|
||||
super.registerDataChange()
|
||||
// lifecycleScope.launch {
|
||||
// viewModel.identifiedFoodInfoList2.collect { list ->
|
||||
// updateFoodInfo(list)
|
||||
// }
|
||||
// }
|
||||
lifecycleScope.launch {
|
||||
viewModel.identifiedFoodInfoList.collect { list ->
|
||||
binding.recyclerview
|
||||
adapter.updateData(list)
|
||||
// presentation.step2()
|
||||
if (list.isNotEmpty()) {
|
||||
checkedItem = list[0]
|
||||
checkedItem!!.photoUri = lastPhotoUri
|
||||
updateCurrentFood(checkedItem)
|
||||
lastPhotoUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.getIdentifiedFoodList()
|
||||
SensorScaleUtils.startScale { weight ->
|
||||
// Timber.d("weight = $weight")
|
||||
if (weight > 0.0) {
|
||||
if (canIdentify) {
|
||||
canIdentify = false
|
||||
viewModel.getIdentifiedFoodList()
|
||||
Timber.d("registerDataChange weight = $weight")
|
||||
presentation?.updateWeight(weight)
|
||||
|
||||
if (weight <= 0.005) {//余量取餐,检测到秤上没有东西,重新启动识别菜品 && presentation?.mealPickupMode == 1
|
||||
isRecognitionFood = true
|
||||
}
|
||||
|
||||
if (weight - lastWeight > 0.05 && isRecognitionFood) { // 大于50g
|
||||
if (presentation?.mealPickupMode == 1) {
|
||||
isRecognitionFood = false
|
||||
}
|
||||
debouncer.debounce {
|
||||
recognizeFood()
|
||||
}
|
||||
}
|
||||
lastWeight = weight
|
||||
}
|
||||
}
|
||||
|
||||
private fun recognizeFood() {
|
||||
LightManager.openRedLight()
|
||||
LightManager.openGreenLight()
|
||||
takePhoto { photoUri ->
|
||||
lastPhotoUri = photoUri
|
||||
Timber.d("registerDataChange photoUri = ${photoUri.path}")
|
||||
// viewModel.getIdentifiedFoodList()
|
||||
|
||||
ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap ->
|
||||
Timber.d("registerDataChange photoUri 拿到bitmap")
|
||||
// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
|
||||
Timber.d("registerDataChange photoUri bitmap裁剪完成")
|
||||
val file = BitmapSaver.saveToAppFilesDir(
|
||||
bitmap,
|
||||
this,
|
||||
"IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||||
)
|
||||
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
|
||||
//val nameList = FoodModule.queryFood(bitmap)
|
||||
val scoreList = FoodModule.getFoodScoreList(bitmap)
|
||||
Timber.d(
|
||||
"registerDataChange photoUri 拿到识别数据:${
|
||||
GsonUtils.toJson(
|
||||
scoreList
|
||||
)
|
||||
}"
|
||||
)
|
||||
val nameList = scoreList.map { it.name }
|
||||
val foodName = nameList.joinToString(separator = ",")
|
||||
|
||||
Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
|
||||
if (TextUtils.isEmpty(foodName)) {
|
||||
LightManager.closeGreenLight()
|
||||
LightManager.closeRedLight()
|
||||
binding.layoutRescan.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.layoutRescan.visibility = View.GONE
|
||||
viewModel.getFoodInfo(foodName) { list ->
|
||||
runOnUiThread {
|
||||
updateFoodInfo(list, scoreList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFoodInfo(list: List<FoodInfo>, scoreList: List<IdNameScore>) {
|
||||
Timber.d("registerDataChange识别后查询接口数据:${GsonUtils.toJson(list)}")
|
||||
list.forEach { foodInfo ->
|
||||
val scoreItem = scoreList.firstOrNull { it.name == foodInfo.foodName }
|
||||
val score = scoreItem?.score ?: 0.0
|
||||
foodInfo.score = ((1 - score) * 10000).roundToInt()
|
||||
}
|
||||
adapter.updateData(list)
|
||||
if (list.isNotEmpty()) {
|
||||
checkedItem = list[0]
|
||||
checkedItem!!.photoUri = lastPhotoUri
|
||||
updateCurrentFood(checkedItem)
|
||||
lastPhotoUri = null
|
||||
LightManager.closeRedLight()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +303,8 @@ class MainActivity : AppCompatActivity() {
|
||||
items = emptyList(),
|
||||
bindingInflater = ItemFoodInfoBinding::inflate,
|
||||
bindCallback = { item, position ->
|
||||
this.tvName.text = item.foodName
|
||||
val score = (item.score / 100.0).format2String(2)
|
||||
this.tvName.text = "${item.foodName}\n${score}%"
|
||||
if (item.id != checkedItem?.id) {
|
||||
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
|
||||
this.tvName.setTextColor(resources.getColor(R.color.identify_normal))
|
||||
@@ -186,6 +317,7 @@ class MainActivity : AppCompatActivity() {
|
||||
//
|
||||
this.llRoot.setOnClickListener {
|
||||
Timber.d("itemClick ${item.foodName}, position = $position")
|
||||
item.photoUri = null
|
||||
checkedItem = item
|
||||
adapter.notifyDataSetChanged()
|
||||
updateCurrentFood(item)
|
||||
@@ -217,7 +349,7 @@ class MainActivity : AppCompatActivity() {
|
||||
// 仅在步骤1时传递
|
||||
if (isAnalyzing) {
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
presentation.updateImage(bitmap) // 更新副屏
|
||||
presentation?.updateImage(bitmap) // 更新副屏
|
||||
}
|
||||
imageProxy.close() // 必须关闭以释放资源
|
||||
}
|
||||
@@ -257,9 +389,9 @@ class MainActivity : AppCompatActivity() {
|
||||
if (isAnalyzing) return
|
||||
isAnalyzing = true
|
||||
imageAnalysis?.setAnalyzer(executor) { imageProxy ->
|
||||
if (presentation.currentStep == 1) {
|
||||
if (presentation?.currentStep == 1) {
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
presentation.updateImage(bitmap)
|
||||
presentation?.updateImage(bitmap)
|
||||
}
|
||||
imageProxy.close()
|
||||
}
|
||||
@@ -289,24 +421,35 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
// 拍照功能
|
||||
private fun takePhoto() {
|
||||
private fun takePhoto(callback: (Uri) -> Unit) {
|
||||
Timber.d("takePhoto")
|
||||
// 临时停止分析以避免干扰
|
||||
isAnalyzing = false
|
||||
|
||||
// 创建输出选项
|
||||
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(
|
||||
File(
|
||||
cacheDir.path, "IMG_${System.currentTimeMillis()}.jpg"
|
||||
val executor = ContextCompat.getMainExecutor(this)
|
||||
val cacheDir = cacheDir
|
||||
val photoFile = File.createTempFile(
|
||||
"IMG_${System.currentTimeMillis()}",
|
||||
".jpg",
|
||||
cacheDir
|
||||
)
|
||||
).build()
|
||||
|
||||
val cacheOutputOptions =
|
||||
ImageCapture.OutputFileOptions.Builder(photoFile).build()
|
||||
// 执行拍照
|
||||
imageCapture?.takePicture(
|
||||
outputFileOptions,
|
||||
cacheOutputOptions,
|
||||
executor,
|
||||
object : ImageCapture.OnImageSavedCallback {
|
||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||
runOnUiThread {
|
||||
ToastUtils.showToast("照片保存成功: ${outputFileResults.savedUri}")
|
||||
val savedUri = outputFileResults.savedUri
|
||||
Timber.d("takePhoto savedUri = $savedUri")
|
||||
// ToastUtils.showToast("照片保存成功: $savedUri")
|
||||
if (savedUri != null) {
|
||||
callback(savedUri)
|
||||
}
|
||||
}
|
||||
// 恢复图像分析
|
||||
isAnalyzing = true
|
||||
@@ -340,18 +483,28 @@ class MainActivity : AppCompatActivity() {
|
||||
viewModel = viewModel,
|
||||
recognizeViewModel = viewModels<RecognizeViewModel>().value
|
||||
)
|
||||
presentation.setStepChangeCallback { step ->
|
||||
presentation!!.setStepChangeCallback { step ->
|
||||
if (step == 1) {
|
||||
Timber.d("setStepChangeCallback${step}")
|
||||
updateCurrentFood(null)
|
||||
bottomSheetDialog?.dismiss()
|
||||
}
|
||||
}
|
||||
presentation.show()
|
||||
presentation!!.show()
|
||||
} else {
|
||||
ToastUtils.showToast("获取副屏失败")
|
||||
}
|
||||
}
|
||||
|
||||
fun clearFoodList() {
|
||||
adapter.updateData(listOf())
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
SensorScaleUtils.closeScale()
|
||||
presentation.dismiss()
|
||||
presentation?.dismiss()
|
||||
super.onDestroy()
|
||||
shutdownCamera()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.sw.dualscreen.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import com.chad.library.adapter4.BaseQuickAdapter
|
||||
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||
import com.sw.dualscreen.R
|
||||
import com.sw.dualscreen.databinding.ListItemCollectedDataBinding
|
||||
import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
|
||||
import com.sw.dualscreen.objbox.CollectedFoodBean
|
||||
import com.sw.dualscreen.objbox.FoodCollectionBean
|
||||
|
||||
class CollectedFoodAdapter (var list: MutableList<CollectedFoodBean>) :
|
||||
BaseQuickAdapter<CollectedFoodBean, CollectedFoodAdapter.VH>(list) {
|
||||
|
||||
inner class VH(var binding: ListItemCollectedDataBinding) : QuickViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val binding = ListItemCollectedDataBinding.inflate(inflater, parent, false)
|
||||
return VH(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodBean?) {
|
||||
holder.binding.tvFoodName.text = item?.foodName
|
||||
holder.binding.divider.run {
|
||||
visibility = if (position == list.size - 1) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.sw.dualscreen.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import com.chad.library.adapter4.BaseQuickAdapter
|
||||
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||
import com.sw.dualscreen.R
|
||||
import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
|
||||
import com.sw.dualscreen.objbox.FoodCollectionBean
|
||||
|
||||
class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
|
||||
BaseQuickAdapter<FoodCollectionBean, FoodCollectionAdapter.VH>(list) {
|
||||
|
||||
inner class VH(var binding: ListItemFoodCollectionBinding) : QuickViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val binding = ListItemFoodCollectionBinding.inflate(inflater, parent, false)
|
||||
return VH(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int, item: FoodCollectionBean?) {
|
||||
val binding = holder.binding
|
||||
item?.let {
|
||||
binding.ivFinish.visibility = if (it.isFinish) View.VISIBLE else View.GONE
|
||||
binding.ivDelete.visibility = if (it.isShowCamera) View.GONE else View.VISIBLE
|
||||
binding.imageView.run {
|
||||
if (it.isShowCamera) {
|
||||
scaleType = ImageView.ScaleType.CENTER
|
||||
setImageResource(R.drawable.ic_camera256)
|
||||
} else {
|
||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
//setImageURI(it.imageUri)
|
||||
setImageBitmap(it.bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.sw.dualscreen.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import com.sw.dualscreen.R
|
||||
import com.sw.dualscreen.ext.dp
|
||||
import com.sw.dualscreen.ext.hideKeyboard
|
||||
|
||||
abstract class BaseDialog(
|
||||
context: Context,
|
||||
var defWidth: Int = 400.dp,
|
||||
var defHeight: Int = 260.dp
|
||||
) : Dialog(context, R.style.DialogTheme) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val rootView = getRootView()
|
||||
setContentView(rootView)
|
||||
setCancelable(false)
|
||||
setCanceledOnTouchOutside(false)
|
||||
window?.apply {
|
||||
setLayout(defWidth, defHeight)
|
||||
setBackgroundDrawable(ColorDrawable())
|
||||
setGravity(Gravity.CENTER)
|
||||
}
|
||||
rootView.setOnClickListener { it.hideKeyboard() }
|
||||
initView()
|
||||
}
|
||||
|
||||
abstract fun getRootView(): View
|
||||
|
||||
abstract fun initView()
|
||||
|
||||
// fun getReceiptActivity(): GoodsListActivity? {
|
||||
// var ctx = context
|
||||
// while (ctx is ContextWrapper) {
|
||||
// ctx = ctx.baseContext
|
||||
// if (ctx is GoodsListActivity) {
|
||||
// return ctx
|
||||
// }
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.sw.dualscreen.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import com.sw.dualscreen.databinding.DialogWarnBinding
|
||||
|
||||
class WarnDialog(
|
||||
context: Context,
|
||||
var content:String,
|
||||
var cancelBlock: () -> Unit = {},
|
||||
var confirmBlock: () -> Unit = {},
|
||||
) : BaseDialog(context = context) {
|
||||
|
||||
private lateinit var binding: DialogWarnBinding
|
||||
override fun getRootView(): View {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
binding = DialogWarnBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
binding.tvWarnContent.text = content
|
||||
binding.btnCancel.setOnClickListener {
|
||||
cancelBlock()
|
||||
dismiss()
|
||||
}
|
||||
binding.btnConfirm.setOnClickListener {
|
||||
confirmBlock()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sw.dualscreen.ext
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
@@ -7,8 +8,17 @@ import android.graphics.ImageFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.YuvImage
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.Dimension
|
||||
import androidx.camera.core.ImageProxy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
@@ -56,6 +66,7 @@ val Float.sp: Float
|
||||
)
|
||||
|
||||
fun Double.format2String(): String = "%.1f".format(this)
|
||||
fun Double.format2String(num:Int): String = "%.${num}f".format(this)
|
||||
|
||||
// 添加扩展函数
|
||||
fun ImageProxy.toSafeBitmap(): Bitmap {
|
||||
@@ -82,3 +93,30 @@ fun ImageProxy.toSafeBitmap(): Bitmap {
|
||||
val imageBytes = out.toByteArray()
|
||||
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
}
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
||||
this.clearFocus() // 清除焦点避免键盘再次弹出
|
||||
}
|
||||
|
||||
fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
|
||||
setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
// 处理搜索逻辑
|
||||
searchCallback()
|
||||
return@setOnEditorActionListener true // 阻止事件继续传递
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
|
||||
var job: Job? = null
|
||||
setOnClickListener {
|
||||
job?.cancel()
|
||||
job = CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(delay)
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ package com.sw.dualscreen.ext
|
||||
* 多于2个字:张*某
|
||||
*/
|
||||
fun String?.maskName(): String {
|
||||
if (this == null || this.isEmpty()) return ""
|
||||
if (this == null || this.isEmpty()) return "-"
|
||||
|
||||
return when {
|
||||
length == 2 -> "${this[0]}*"
|
||||
@@ -35,3 +35,12 @@ fun String?.maskPhone(): String {
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun String?.toSafeDouble(): Double {
|
||||
if (this == null) return 0.0
|
||||
return try {
|
||||
toDouble()
|
||||
} catch (e: Exception) {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.sw.dualscreen.model;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IActivityResult {
|
||||
|
||||
interface LaunchActivityCallback {
|
||||
void callback(ActivityResult result);
|
||||
}
|
||||
|
||||
interface LaunchTackPictureCallback {
|
||||
void callback(boolean result);
|
||||
}
|
||||
|
||||
interface RequestPermissionCallback {
|
||||
void callback(boolean result);
|
||||
}
|
||||
|
||||
interface RequestMultiplePermissionsCallback {
|
||||
void callback(Map<String,Boolean> result);
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,5 @@ data class UserNutritionParam(
|
||||
val faceEndTime: Long,
|
||||
val eatWeight: Double,
|
||||
val foodWeight: Double,
|
||||
val deviceId: String = ""
|
||||
val deviceId: String = GlobalData.deviceId
|
||||
) : Parcelable
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sw.dualscreen.model.response
|
||||
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.text.TextUtils
|
||||
import com.google.gson.annotations.SerializedName
|
||||
@@ -23,7 +24,11 @@ data class FoodInfo(
|
||||
@SerializedName("id")
|
||||
val id: String? = "",
|
||||
@SerializedName("imgUrl")
|
||||
val imgUrl: String? = "",
|
||||
var imgUrl: String? = "",
|
||||
/**
|
||||
* 拍照结果
|
||||
*/
|
||||
var photoUri: Uri? = null,
|
||||
@SerializedName("stFoodInfoMaterial")
|
||||
val stFoodInfoMaterial: StFoodInfoMaterial? = StFoodInfoMaterial(),
|
||||
@SerializedName("stFoodInfoPagoda")
|
||||
@@ -33,7 +38,8 @@ data class FoodInfo(
|
||||
@SerializedName("stFoodInfoSetting")
|
||||
val stFoodInfoSetting: StFoodInfoSetting? = StFoodInfoSetting(),
|
||||
@SerializedName("stFoodInfoSpecificationList")
|
||||
val stFoodInfoSpecificationList: List<StFoodInfoSpecification?>? = listOf()
|
||||
val stFoodInfoSpecificationList: List<StFoodInfoSpecification?>? = listOf(),
|
||||
var score: Int = 0
|
||||
) : Parcelable {
|
||||
@Parcelize
|
||||
data class FoodTypeAndRealIntakeVo(
|
||||
|
||||
@@ -35,7 +35,7 @@ data class UserNutritionData(
|
||||
* 食物类型和实际摄入量清单
|
||||
*/
|
||||
@SerializedName("foodTypeAndRealIntakeVoList")
|
||||
val foodTypeAndRealIntakeVoList: List<String?>? = listOf(),
|
||||
val foodTypeAndRealIntakeVoList: List<FoodTypeAndRealIntakeVo?>? = listOf(),
|
||||
@SerializedName("foodWeight")
|
||||
val foodWeight: String? = "",
|
||||
@SerializedName("message")
|
||||
@@ -66,7 +66,7 @@ data class UserNutritionData(
|
||||
@SerializedName("stFoodInfoPagoda")
|
||||
val stFoodInfoPagoda: StFoodInfoPagoda? = StFoodInfoPagoda(),
|
||||
@SerializedName("stUserFoodInfoList")
|
||||
val stUserFoodInfoList: List<String?>? = listOf(),
|
||||
val stUserFoodInfoList: List<StUserFoodInfo?>? = listOf(),
|
||||
/**
|
||||
* 每日总热量
|
||||
*/
|
||||
@@ -104,6 +104,26 @@ data class UserNutritionData(
|
||||
if (TextUtils.isEmpty(totalEnergyCalculateScore)) 0.0
|
||||
else totalEnergyCalculateScore!!.toDouble()
|
||||
|
||||
@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 StFoodInfoPagoda(
|
||||
/**
|
||||
@@ -178,4 +198,56 @@ data class UserNutritionData(
|
||||
else meat!!.toDouble()
|
||||
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class StUserFoodInfo(
|
||||
@SerializedName("canteenId")
|
||||
val canteenId: String? = "",
|
||||
@SerializedName("createBy")
|
||||
val createBy: String? = "",
|
||||
@SerializedName("createTime")
|
||||
val createTime: String? = "",
|
||||
@SerializedName("dataSource")
|
||||
val dataSource: String? = "",
|
||||
@SerializedName("dataType")
|
||||
val dataType: String? = "",
|
||||
@SerializedName("delFlag")
|
||||
val delFlag: String? = "",
|
||||
@SerializedName("deviceId")
|
||||
val deviceId: String? = "",
|
||||
@SerializedName("dinnerType")
|
||||
val dinnerType: String? = "",
|
||||
@SerializedName("eatDay")
|
||||
val eatDay: String? = "",
|
||||
@SerializedName("eatNum")
|
||||
val eatNum: String? = "",
|
||||
@SerializedName("eatWeight")
|
||||
val eatWeight: Double? = 0.0,
|
||||
@SerializedName("foodId")
|
||||
val foodId: String? = "",
|
||||
@SerializedName("foodImg")
|
||||
val foodImg: String? = "",
|
||||
@SerializedName("foodMaterialId")
|
||||
val foodMaterialId: String? = "",
|
||||
@SerializedName("foodName")
|
||||
val foodName: String? = "",
|
||||
@SerializedName("foodWeight")
|
||||
val foodWeight: String? = "",
|
||||
@SerializedName("id")
|
||||
val id: String? = "",
|
||||
@SerializedName("isSync")
|
||||
val isSync: String? = "",
|
||||
@SerializedName("isSyncCopy")
|
||||
val isSyncCopy: String? = "",
|
||||
@SerializedName("specId")
|
||||
val specId: String? = "",
|
||||
@SerializedName("stBasicDiningInformationUserESVo")
|
||||
val stBasicDiningInformationUserESVo: String? = "",
|
||||
@SerializedName("stallType")
|
||||
val stallType: String? = "",
|
||||
@SerializedName("type")
|
||||
val type: String? = "",
|
||||
@SerializedName("userId")
|
||||
val userId: String? = ""
|
||||
) : Parcelable
|
||||
}
|
||||
@@ -8,10 +8,15 @@ import com.sw.dualscreen.model.response.EquipmentInfo
|
||||
import com.sw.dualscreen.model.response.FoodInfo
|
||||
import com.sw.dualscreen.model.response.UserFaceInfo
|
||||
import com.sw.dualscreen.model.response.UserNutritionData
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Multipart
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Part
|
||||
import retrofit2.http.PartMap
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.Url
|
||||
|
||||
@@ -97,6 +102,29 @@ interface ApiService {
|
||||
@POST
|
||||
suspend fun postUserNutritionData(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/userEatFood/doubleBalance/face/stall",
|
||||
@Body param: UserNutritionParam
|
||||
@Body param: List<UserNutritionParam>
|
||||
): ApiResponse<String>
|
||||
|
||||
/**
|
||||
* 获取菜品信息
|
||||
*/
|
||||
@GET
|
||||
suspend fun getFoodInfo(
|
||||
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall/v2",
|
||||
@Query("restId") restId: String,
|
||||
@Query("foodNames") foodName: String,
|
||||
): ApiResponse<List<FoodInfo>>
|
||||
|
||||
|
||||
/**
|
||||
* 提交采集图片数据
|
||||
*/
|
||||
@Multipart
|
||||
@POST
|
||||
suspend fun postImageData(
|
||||
@Url url: String = "http://192.168.1.201:14801/terminal/neglect/dishCollectionVectorData/add",
|
||||
@PartMap params: Map<String, @JvmSuppressWildcards RequestBody>,
|
||||
@Part image: MultipartBody.Part?
|
||||
): ApiResponse<String>
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.sw.dualscreen.network.interceptor
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.sw.dualscreen.GlobalKey
|
||||
import com.sw.inbound.utils.SPUtil
|
||||
import com.sw.dualscreen.utils.SPUtil
|
||||
import com.sw.plate.App
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.sw.dualscreen.objbox
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
|
||||
data class FoodClassInfo(
|
||||
var class_names: List<String>,
|
||||
var class_to_idx: Map<String, Int>,
|
||||
var idx_to_class: Map<String, String>
|
||||
)
|
||||
|
||||
//data class FoodQueryResult(
|
||||
// var id: Long,
|
||||
// var name: String?,
|
||||
// var foodIdx: Int,
|
||||
// var score: Double
|
||||
//)
|
||||
|
||||
data class FoodCollectionBean(
|
||||
var imageUri: Uri? = null,
|
||||
var bitmap: Bitmap? = null,
|
||||
var imageVector: FloatArray? = null,
|
||||
var isShowCamera: Boolean = false,
|
||||
var isFinish:Boolean = false
|
||||
)
|
||||
|
||||
data class CollectedFoodBean(
|
||||
var foodName: String?,
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.sw.dualscreen.objbox
|
||||
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.HnswIndex
|
||||
import io.objectbox.annotation.Id
|
||||
import io.objectbox.annotation.VectorDistanceType
|
||||
|
||||
@Entity
|
||||
data class Food(
|
||||
@Id var id: Long = 0,
|
||||
var name: String? = null,
|
||||
var foodIdx: Int = 0,
|
||||
@HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT)
|
||||
var foodVector: FloatArray? = null
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Food
|
||||
|
||||
if (id != other.id) return false
|
||||
if (foodIdx != other.foodIdx) return false
|
||||
if (name != other.name) return false
|
||||
if (!foodVector.contentEquals(other.foodVector)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + foodIdx
|
||||
result = 31 * result + (name?.hashCode() ?: 0)
|
||||
result = 31 * result + (foodVector?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.sw.dualscreen.objbox
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.SparseLongArray
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.sw.dualscreen.MyApp
|
||||
import com.sw.dualscreen.utils.AssetsTool
|
||||
import com.sw.dualscreen.utils.GsonUtils
|
||||
import com.sw.dualscreen.utils.ImageUtil
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import io.objectbox.query.Query
|
||||
import org.pytorch.IValue
|
||||
import org.pytorch.Module
|
||||
import org.pytorch.torchvision.TensorImageUtils
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
object FoodModule {
|
||||
|
||||
private const val THRESHOLD = 0.8
|
||||
private lateinit var module_mobile: Module
|
||||
private lateinit var box: Box<Food>
|
||||
private lateinit var embeddingsList: List<List<Float>>
|
||||
private lateinit var labelsList: IntArray
|
||||
private lateinit var classInfo: FoodClassInfo
|
||||
val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
|
||||
val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
|
||||
val DEFAULT_FOOD_INDEX = -1
|
||||
|
||||
fun init(context: Context) {
|
||||
Thread {
|
||||
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
|
||||
box = ObjectBox.boxStore.boxFor(Food::class)
|
||||
//if (box.all.isNotEmpty()) {
|
||||
// box.removeAll()
|
||||
//}
|
||||
//if (box.all.isEmpty()) {
|
||||
// initDefFoodData(context)
|
||||
//}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun uri2FloatArray(uri: Uri): FloatArray? {
|
||||
return MyApp.instance?.let { context ->
|
||||
ImageUtil.uriToBitmap(context, uri)?.let {
|
||||
bitmap2FloatArray(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bitmap2FloatArray(bitmap: Bitmap): FloatArray {
|
||||
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
|
||||
bitmap,
|
||||
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
|
||||
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
|
||||
)
|
||||
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
|
||||
return outputTensor.dataAsFloatArray
|
||||
}
|
||||
|
||||
fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
|
||||
return uri2FloatArray(uri)?.let {
|
||||
queryFood(it, queryCount)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回识别物品名称列表
|
||||
*/
|
||||
fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List<String> {
|
||||
val floatArray = bitmap2FloatArray(bitmap)
|
||||
return queryFood(floatArray, queryCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回识别物品IdNameScore对象列表
|
||||
*/
|
||||
fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
|
||||
val floatArray = bitmap2FloatArray(bitmap)
|
||||
return queryFoodNameScore(floatArray, queryCount)
|
||||
}
|
||||
|
||||
fun queryFoodNameScore(floatArray: FloatArray, queryCount: Int = 15): List<IdNameScore> {
|
||||
val query: Query<Food> =
|
||||
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
|
||||
//查询比较分数
|
||||
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
|
||||
val idScoreList = query.findIdsWithScores()
|
||||
val nameScoreList = mutableListOf<IdNameScore>()
|
||||
idScoreList.forEach {
|
||||
nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score))
|
||||
}
|
||||
Timber.tag("FoodModule").d("registerDataChange,queryFood数据:${GsonUtils.toJson(nameScoreList)}")
|
||||
return nameScoreList
|
||||
}
|
||||
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
|
||||
val floatArray = bitmap2FloatArray(bitmap)
|
||||
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||
val maxScoreList = nameScoreList
|
||||
.filter { it.score < 1 - THRESHOLD }
|
||||
.groupBy { it.name }
|
||||
.map { (_, value) -> value.minByOrNull { it.score }!! }
|
||||
.toMutableList()
|
||||
val map = mutableMapOf<String, Int>()
|
||||
nameScoreList.forEach {
|
||||
val key = it.name
|
||||
val count = map[key] ?: 0
|
||||
map[key] = count + 1
|
||||
}
|
||||
val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
|
||||
val firstFood = nameScoreList[0].name
|
||||
orderList.remove(firstFood)
|
||||
orderList.add(0, firstFood)
|
||||
|
||||
val sortedScoreList = maxScoreList.sortedWith(compareBy {
|
||||
orderList.indexOf(it.name)
|
||||
})
|
||||
//Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
|
||||
return sortedScoreList
|
||||
}
|
||||
fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
|
||||
val query: Query<Food> =
|
||||
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
|
||||
//查询比较分数
|
||||
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
|
||||
val map = mutableMapOf<String, Int>()
|
||||
query.findIdsWithScores().forEach {
|
||||
val food = box.get(it.id)
|
||||
Timber.d("${food.name}|${food.foodIdx}|${it.score}")
|
||||
if (1 - it.score >= THRESHOLD) {
|
||||
//FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
|
||||
food.name?.let { key ->
|
||||
val count = map[key] ?: 0
|
||||
map.put(key, count + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
val list = map.entries.sortedByDescending { it.value }.map { it.key }
|
||||
return list
|
||||
|
||||
// val map = mutableMapOf<String, Int>()
|
||||
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||
// nameScoreList.filter { it.score < 0.05 }.forEach {
|
||||
// val count = map[it.name] ?: 0
|
||||
// map[it.name] = count + 1
|
||||
// }
|
||||
// val list = map.entries.sortedByDescending { it.value }.map { it.key }
|
||||
// return list
|
||||
}
|
||||
|
||||
data class IdNameScore(
|
||||
val id:Long,
|
||||
var name:String,
|
||||
val score: Double
|
||||
)
|
||||
|
||||
fun initDefFoodData(context: Context, action:()-> Unit={}) {
|
||||
val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
|
||||
if (count > 0) {
|
||||
return
|
||||
}
|
||||
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
|
||||
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
|
||||
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
|
||||
|
||||
embeddingsList =
|
||||
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
|
||||
labelsList = Gson().fromJson(labelsJson, IntArray::class.java)
|
||||
classInfo =
|
||||
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
|
||||
|
||||
val foodMap = classInfo.idx_to_class
|
||||
embeddingsList.forEachIndexed { index, floatList ->
|
||||
val classIdx = labelsList[index]
|
||||
val foodName = foodMap["$classIdx"]
|
||||
val array = floatList.toFloatArray()
|
||||
box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
|
||||
}
|
||||
action()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ,此方法的主要目的是:从assets 拷贝到 app的cache目录
|
||||
* @param context
|
||||
* @param fileName
|
||||
* @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
|
||||
*
|
||||
* 不可能反正SD
|
||||
*/
|
||||
fun copyAssetToCache(context: Context, fileName: String): String? {
|
||||
// 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
|
||||
val cacheDir = context.getCacheDir()
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
|
||||
}
|
||||
val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
|
||||
if (outPath.exists()) {
|
||||
outPath.delete() // TODO 如果该文件已经存在,就删掉
|
||||
}
|
||||
var `is`: InputStream? = null // 读取
|
||||
var fos: FileOutputStream? = null // 写入
|
||||
try {
|
||||
// 创建文件,如果创建成功,就返回true
|
||||
val res = outPath.createNewFile()
|
||||
if (res) {
|
||||
`is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
|
||||
fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
|
||||
val buf = ByteArray(`is`.available()) // 缓存区
|
||||
var byteCount: Int
|
||||
|
||||
// 开始循环读取
|
||||
while ((`is`.read(buf).also { byteCount = it }) != -1) {
|
||||
fos.write(buf, 0, byteCount)
|
||||
}
|
||||
return outPath.getAbsolutePath()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
try {
|
||||
// TODO 一定要记得关闭资源,为了不去性能的磨损
|
||||
fos!!.flush()
|
||||
`is`!!.close()
|
||||
fos.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2024 ObjectBox Ltd. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.sw.dualscreen.objbox
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import io.objectbox.BoxStore
|
||||
import io.objectbox.BoxStoreBuilder
|
||||
import io.objectbox.android.Admin
|
||||
import io.objectbox.android.ObjectBoxLiveData
|
||||
import io.objectbox.exception.DbException
|
||||
import io.objectbox.exception.FileCorruptException
|
||||
import io.objectbox.sync.Sync
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import kotlin.also
|
||||
import kotlin.io.copyTo
|
||||
import kotlin.io.inputStream
|
||||
import kotlin.io.outputStream
|
||||
import kotlin.io.use
|
||||
import kotlin.jvm.java
|
||||
import kotlin.jvm.javaClass
|
||||
|
||||
/**
|
||||
* Singleton to keep BoxStore reference and provide current list of Notes Objects.
|
||||
* Inserts demo data if no Objects are stored.
|
||||
*/
|
||||
object ObjectBox {
|
||||
|
||||
// 确保所有设备/版本使用相同的objectbox-models/default.json
|
||||
// 避免直接修改自动生成的MyObjectBox类
|
||||
// 跨版本升级时使用boxStore.runInTx执行数据迁移
|
||||
|
||||
|
||||
private const val TAG = "ObjectBox"
|
||||
|
||||
lateinit var boxStore: BoxStore
|
||||
private set
|
||||
|
||||
/**
|
||||
* If building the [boxStore] failed, contains the thrown error message.
|
||||
*/
|
||||
var dbExceptionMessage: String? = null
|
||||
private set
|
||||
|
||||
fun init(context: Context) {
|
||||
// On Android make sure to pass a Context when building the Store.
|
||||
boxStore = try {
|
||||
MyObjectBox.builder()
|
||||
.androidContext(context.applicationContext)
|
||||
.build()
|
||||
} catch (e: DbException) {
|
||||
if (e.javaClass.equals(DbException::class.java) || e is FileCorruptException) {
|
||||
// Failed to build BoxStore due to database file issue, store message;
|
||||
// checked in NoteListActivity to notify user.
|
||||
dbExceptionMessage = e.toString()
|
||||
return
|
||||
} else {
|
||||
// Failed to build BoxStore due to developer error.
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// if (BuildConfig.DEBUG) {
|
||||
var syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
|
||||
Log.d(TAG,"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
|
||||
// Enable ObjectBox Admin on debug builds.
|
||||
// https://docs.objectbox.io/data-browser
|
||||
Admin(boxStore).start(context.applicationContext)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If the database file is not in use, compresses (GZIP) and copies it to the given [target].
|
||||
*/
|
||||
fun copyAndGzipDatabaseFileTo(target: File, context: Context): Boolean {
|
||||
if (BoxStore.isDatabaseOpen(context, null)) {
|
||||
// Do not copy if database file is still in use.
|
||||
// If it would be open, the copy will likely get corrupted
|
||||
// as BoxStore may currently write data to the file.
|
||||
Log.e(TAG, "Database file is still in use, can not copy.")
|
||||
return false
|
||||
}
|
||||
|
||||
// If a name was given when building BoxStore use that instead of the default below.
|
||||
val dbName = BoxStoreBuilder.DEFAULT_NAME
|
||||
File(context.filesDir, "objectbox/$dbName/data.mdb").inputStream().use { input ->
|
||||
target.parentFile?.mkdirs()
|
||||
GZIPOutputStream(target.outputStream()).use { output ->
|
||||
input.copyTo(output, DEFAULT_BUFFER_SIZE)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,18 +8,14 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Point
|
||||
import android.hardware.Camera
|
||||
import android.hardware.camera2.CameraDevice
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
@@ -32,13 +28,14 @@ import com.sw.dualscreen.databinding.PresentationSecondaryScreenBinding
|
||||
import com.sw.dualscreen.ext.dp
|
||||
import com.sw.dualscreen.ext.format2String
|
||||
import com.sw.dualscreen.ext.maskName
|
||||
import com.sw.dualscreen.model.request.UserNutritionParam
|
||||
import com.sw.dualscreen.model.response.DinnerTypeInfo
|
||||
import com.sw.dualscreen.model.response.FoodInfo
|
||||
import com.sw.dualscreen.model.response.UserNutritionData
|
||||
import com.sw.dualscreen.sdk.SensorScaleUtils
|
||||
import com.sw.dualscreen.utils.Debouncer
|
||||
import com.sw.dualscreen.utils.GlideUtils
|
||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
||||
import com.sw.plate.utils.LightManager
|
||||
import com.sw.plate.utils.ToastUtils
|
||||
import com.sw.plate.utils.arcface.ConfigUtil
|
||||
import com.sw.plate.utils.arcface.ErrorCodeUtil
|
||||
@@ -49,9 +46,11 @@ import com.sw.plate.utils.arcface.PreviewConfig
|
||||
import com.sw.plate.utils.arcface.camera.CameraListener
|
||||
import com.sw.plate.utils.arcface.camera.DualCameraHelper
|
||||
import com.sw.plate.utils.arcface.face.constants.LivenessType
|
||||
import com.sw.plate.utils.arcface.face.model.CompareResult
|
||||
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo
|
||||
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
|
||||
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -67,22 +66,28 @@ class SecondaryScreenPresentation(
|
||||
val recognizeViewModel: RecognizeViewModel
|
||||
) : Presentation(context, display), ViewTreeObserver.OnGlobalLayoutListener {
|
||||
private lateinit var binding: PresentationSecondaryScreenBinding
|
||||
private var cameraDevice: CameraDevice? = null
|
||||
|
||||
// private var cameraCaptureSession: CameraCaptureSession? = null
|
||||
private lateinit var cameraManager: CameraManager
|
||||
private var cameraIdList: Array<String> = arrayOf()
|
||||
|
||||
// 当前食物信息
|
||||
private var currentFood: FoodInfo? = null
|
||||
private var userNutritionData: UserNutritionData? = null
|
||||
private var dinnerType: DinnerTypeInfo? = null
|
||||
private var dinnerTypeInfo: DinnerTypeInfo? = null
|
||||
private lateinit var itemUserNutritionBinding: ItemUserNutritionBinding
|
||||
private lateinit var stepChangeCallback: (Int) -> Unit
|
||||
|
||||
|
||||
private var detectWeight = 0.0 //识别菜品时的重量
|
||||
private var lastWeight = 0.0 // 上一次的计算热量结果
|
||||
private val debouncer = Debouncer(500)
|
||||
private var recognitionTime: Long = 0L // 人脸识别时的时间
|
||||
private var recognitionWeight: Double = 0.0 // 人脸识别时的重量
|
||||
private var lastFaceTrackId: Int = -1 // 上一次的人脸信息
|
||||
var mealPickupMode: Int = 0 // 取餐模式 0 即放即取 1 余量取餐
|
||||
|
||||
// 当前步骤
|
||||
var currentStep: Int = 0
|
||||
|
||||
private var isGoStep1 = false;//防止持续调用
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -90,25 +95,35 @@ class SecondaryScreenPresentation(
|
||||
setContentView(binding.root)
|
||||
window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||||
initView()
|
||||
// binding.includeUserHeat.ivHeat.showText("1070千卡")
|
||||
setupArcCamera()
|
||||
registerDataChange()
|
||||
}
|
||||
|
||||
private fun registerDataChange() {
|
||||
activity.lifecycleScope.launch {
|
||||
viewModel.loadFaceResult.collect { needUpdate ->
|
||||
if (needUpdate) {
|
||||
recognizeViewModel.refreshFaceList()
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.lifecycleScope.launch {
|
||||
viewModel.nutritionData.collect {
|
||||
Timber.d("registerDataChange nutritionData = $it")
|
||||
if (it == null) return@collect
|
||||
userNutritionData = it
|
||||
if (it == null) return@collect
|
||||
step3()
|
||||
itemUserNutritionBinding.tvUserName.text = it.userName.maskName()
|
||||
itemUserNutritionBinding.tvRecommendHeat.text =
|
||||
"推荐热量:${it.recommendMin}-${it.recommendMax}"
|
||||
|
||||
updateWeight(lastWeight / 1000)
|
||||
}
|
||||
}
|
||||
activity.lifecycleScope.launch {
|
||||
viewModel.dinnerTypeInfo.collect {
|
||||
dinnerType = it
|
||||
viewModel.dinnerTypeInfo.drop(1).collect {
|
||||
Timber.d("registerDataChange dinnerTypeInfo = $it")
|
||||
dinnerTypeInfo = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,22 +153,9 @@ class SecondaryScreenPresentation(
|
||||
* 启用虹软人脸失败
|
||||
*/
|
||||
private fun setupArcCamera() {
|
||||
cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
// 获取摄像头列表
|
||||
cameraIdList = cameraManager.cameraIdList
|
||||
if (cameraIdList.isEmpty()) {
|
||||
Toast.makeText(context, "没有可用的摄像头", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
cameraIdList.forEach {
|
||||
Timber.d("摄像头id列表 = ${it}")
|
||||
}
|
||||
|
||||
initArcViewModel()
|
||||
initArcView()
|
||||
openRectInfoDraw = true
|
||||
openCamera(cameraIdList.last())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,49 +164,69 @@ class SecondaryScreenPresentation(
|
||||
fun step1() {
|
||||
Timber.d("step1")
|
||||
currentStep = 1
|
||||
LightManager.closeGreenLight()
|
||||
LightManager.closeRedLight()
|
||||
//回复上一次的设置
|
||||
// lastWeight = 0.0
|
||||
dinnerTypeInfo = null
|
||||
currentFood = null
|
||||
userNutritionData = null
|
||||
recognitionWeight = 0.0
|
||||
|
||||
stepChangeCallback(currentStep)
|
||||
binding.tvFoodInfo.text = "菜品识别中"
|
||||
binding.tvFoodName.text = "菜品识别中"
|
||||
binding.ivFaceBg.visibility = View.GONE
|
||||
binding.flPreview.visibility = View.VISIBLE
|
||||
binding.ivImg.visibility = View.VISIBLE
|
||||
binding.flFace.visibility = View.GONE
|
||||
binding.flFace.visibility = View.INVISIBLE
|
||||
binding.tvFaceTip.visibility = View.GONE
|
||||
pauseCamera()
|
||||
|
||||
binding.tvBottomTip.visibility = View.VISIBLE
|
||||
itemUserNutritionBinding.clNutritionData.visibility = View.GONE
|
||||
stopSensorScale()
|
||||
viewModel.getDinnerType()
|
||||
|
||||
activity.clearFoodList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示人脸识别
|
||||
*/
|
||||
fun step2(foodInfo: FoodInfo) {
|
||||
isGoStep1 = true
|
||||
Timber.d("step2")
|
||||
currentStep = 2
|
||||
LightManager.closeRedLight()
|
||||
// LightManager.openGreenLight()
|
||||
dinnerTypeInfo = null
|
||||
userNutritionData = null
|
||||
recognitionWeight = 0.0
|
||||
|
||||
stepChangeCallback(currentStep)
|
||||
currentFood = foodInfo
|
||||
viewModel.getDinnerType()
|
||||
resumeCamera()
|
||||
binding.tvFoodInfo.text = foodInfo.foodName
|
||||
binding.tvFoodName.text = foodInfo.foodName
|
||||
binding.ivImg.visibility = View.GONE
|
||||
|
||||
binding.ivFaceBg.visibility = View.VISIBLE
|
||||
binding.flPreview.visibility = View.GONE
|
||||
|
||||
// binding.flFace.visibility = View.VISIBLE
|
||||
binding.flFace.post {
|
||||
binding.flFace.visibility = View.VISIBLE
|
||||
}
|
||||
binding.ivFaceBg.visibility = View.VISIBLE
|
||||
binding.tvFaceTip.visibility = View.VISIBLE
|
||||
|
||||
setupArcCamera()
|
||||
binding.tvBottomTip.visibility = View.VISIBLE
|
||||
itemUserNutritionBinding.clNutritionData.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun updateFood(foodInfo: FoodInfo?) {
|
||||
Timber.d("step2 updateFood")
|
||||
Timber.d("updateFood")
|
||||
currentFood = foodInfo
|
||||
if (foodInfo != null) {
|
||||
if (currentStep != 2) {
|
||||
if (currentStep == 1) {
|
||||
step2(foodInfo)
|
||||
} else {
|
||||
binding.tvFoodInfo.text = foodInfo.foodName
|
||||
updateFoodInfo(foodInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,141 +236,218 @@ class SecondaryScreenPresentation(
|
||||
*/
|
||||
fun step3() {
|
||||
Timber.d("step3")
|
||||
LightManager.closeRedLight()
|
||||
LightManager.openGreenLight()
|
||||
currentStep = 3
|
||||
stepChangeCallback(currentStep)
|
||||
if (currentFood == null) {
|
||||
ToastUtils.showToast("选中物品为空3")
|
||||
return
|
||||
}
|
||||
binding.tvFoodInfo.text = currentFood!!.foodName
|
||||
|
||||
startSensorScale()
|
||||
|
||||
GlideUtils.loadRoundCornerImage(
|
||||
context,
|
||||
url = currentFood!!.imgUrl,
|
||||
imageView = binding.ivImg,
|
||||
radius = 12
|
||||
)
|
||||
updateFoodInfo(currentFood!!)
|
||||
binding.flPreview.visibility = View.VISIBLE
|
||||
// binding.previewView.visibility = View.GONE
|
||||
binding.ivImg.visibility = View.VISIBLE
|
||||
binding.flFace.visibility = View.GONE
|
||||
binding.flFace.visibility = View.INVISIBLE
|
||||
binding.ivFaceBg.visibility = View.GONE
|
||||
binding.tvFaceTip.visibility = View.GONE
|
||||
|
||||
binding.tvBottomTip.visibility = View.GONE
|
||||
itemUserNutritionBinding.clNutritionData.visibility = View.VISIBLE
|
||||
|
||||
viewModel.getUserNutritionData(
|
||||
userId = userNutritionData!!.userId!!,
|
||||
foodId = currentFood!!.id!!
|
||||
)
|
||||
}
|
||||
|
||||
private fun startSensorScale() {
|
||||
var lastWeight = 0.0
|
||||
val debouncer = Debouncer(1000)
|
||||
SensorScaleUtils.startScale { weight ->
|
||||
if (weight == lastWeight) return@startScale
|
||||
Timber.d("registerDataChange weight = $weight")
|
||||
lastWeight = weight
|
||||
if (currentFood == null) return@startScale
|
||||
if (userNutritionData == null) return@startScale
|
||||
if (dinnerType == null) return@startScale
|
||||
private fun updateFoodInfo(foodInfo: FoodInfo) {
|
||||
binding.tvFoodName.text = foodInfo.foodName
|
||||
if (foodInfo.photoUri != null) {
|
||||
GlideUtils.loadRoundCornerImage(
|
||||
context,
|
||||
url = foodInfo.photoUri,
|
||||
imageView = binding.ivImg,
|
||||
radius = 12
|
||||
)
|
||||
} else {
|
||||
GlideUtils.loadRoundCornerImage(
|
||||
context,
|
||||
url = foodInfo.imgUrl,
|
||||
imageView = binding.ivImg,
|
||||
radius = 12
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWeight(weight: Double) {
|
||||
lastWeight = weight * 1000 // 将千克转成克
|
||||
Timber.d("updateWeight lastWeight = $weight")
|
||||
if (currentStep == 2 &&
|
||||
lastWeight <= 5 &&
|
||||
isGoStep1
|
||||
) {//人脸识别过程中,秤上物品拿走
|
||||
isGoStep1 = false
|
||||
activity.runOnUiThread {
|
||||
Timber.e("切回step1")
|
||||
step1()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (currentFood == null) {
|
||||
Timber.e("updateWeight currentFood is null")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (userNutritionData == null) {
|
||||
Timber.e("updateWeight userNutritionData is null")
|
||||
return
|
||||
}
|
||||
if (dinnerTypeInfo == null) {
|
||||
Timber.e("updateWeight dinnerType is null")
|
||||
return
|
||||
}
|
||||
calculateNutrition(recognitionWeight - lastWeight)
|
||||
}
|
||||
|
||||
fun updateDetectWeight(weight: Double) {
|
||||
detectWeight = weight * 1000 // 将千克转成克
|
||||
}
|
||||
|
||||
/**
|
||||
* 余量取餐
|
||||
*/
|
||||
fun updateMealPickupMode(mode: Int) {
|
||||
Timber.d("updateMealPickupMode mode = $mode")
|
||||
mealPickupMode = mode
|
||||
step1()
|
||||
}
|
||||
|
||||
private fun calculateNutrition(weight1: Double) {
|
||||
var weight = weight1
|
||||
if (weight < 0) weight = 0.0
|
||||
debouncer.debounce {
|
||||
Timber.d("calculateNutrition weight = $weight, recognitionWeight = $recognitionWeight")
|
||||
val dinnerType = dinnerTypeInfo!!.dinnerType!!.dinnerType!!
|
||||
Timber.d("calculateNutrition foodName = ${currentFood!!.foodName}, dinnerType = $dinnerType, userId = ${userNutritionData!!.userId}")
|
||||
val calcResultInfo = UserNutritionUtils.calculateNutrition(
|
||||
currentFood!!,
|
||||
userNutritionData!!,
|
||||
weight,
|
||||
dinnerType = dinnerType!!.dinnerType!!.dinnerType!!
|
||||
dinnerType = dinnerType
|
||||
)
|
||||
Timber.d("calcResultInfo = $calcResultInfo")
|
||||
Timber.d("calculateNutrition calcResultInfo = $calcResultInfo")
|
||||
val columnMax = UserNutritionUtils.getMaxInt(calcResultInfo)
|
||||
|
||||
activity.runOnUiThread {
|
||||
val totalKcal = calcResultInfo.totalKcal
|
||||
val grain = calcResultInfo.grain
|
||||
val fruits = calcResultInfo.fruits
|
||||
val meat = calcResultInfo.meat
|
||||
// 设置热量
|
||||
itemUserNutritionBinding.customKcalColumn.setImageDrawable(
|
||||
true,
|
||||
totalKcal.first < totalKcal.second
|
||||
totalKcal.max < totalKcal.current
|
||||
)
|
||||
itemUserNutritionBinding.totalKcalTv.text = totalKcal.second.format2String()
|
||||
var maxKcal = totalKcal.max
|
||||
Timber.d("maxKcal = $maxKcal")
|
||||
// 避免0作为被除数
|
||||
if (maxKcal <= 0.0) {
|
||||
maxKcal = 1.0
|
||||
}
|
||||
Timber.d("maxKcal 1 = $maxKcal")
|
||||
itemUserNutritionBinding.totalKcalTv.text = totalKcal.current.format2String()
|
||||
var totalKcalHeight = (totalKcal.current / maxKcal).toFloat()
|
||||
if (totalKcalHeight == 0F) {
|
||||
itemUserNutritionBinding.divKcalView.visibility = View.VISIBLE
|
||||
} else {
|
||||
itemUserNutritionBinding.divKcalView.visibility = View.GONE
|
||||
totalKcalHeight += 0.1F
|
||||
}
|
||||
itemUserNutritionBinding.customKcalColumn.setCustomHeightPercent(
|
||||
(totalKcal.second / 100).toFloat(),
|
||||
totalKcalHeight,
|
||||
true
|
||||
)
|
||||
|
||||
// 设置主食
|
||||
itemUserNutritionBinding.customFoodColumn.setImageDrawable(
|
||||
false,
|
||||
grain.first < grain.second
|
||||
grain.max < grain.current
|
||||
)
|
||||
itemUserNutritionBinding.totalFoodTv.text = grain.second.format2String()
|
||||
itemUserNutritionBinding.totalFoodTv.text = grain.current.format2String()
|
||||
var grainHeight = (grain.current / columnMax).toFloat()
|
||||
if (grainHeight == 0F) {
|
||||
itemUserNutritionBinding.divTotalFoodView.visibility = View.VISIBLE
|
||||
} else {
|
||||
itemUserNutritionBinding.divTotalFoodView.visibility = View.GONE
|
||||
grainHeight += 0.1F
|
||||
}
|
||||
itemUserNutritionBinding.customFoodColumn.setCustomHeightPercent(
|
||||
(grain.second / 100).toFloat(),
|
||||
grainHeight,
|
||||
true
|
||||
)
|
||||
|
||||
// 设置果蔬
|
||||
itemUserNutritionBinding.customVegetableColumn.setImageDrawable(
|
||||
false,
|
||||
fruits.first < fruits.second
|
||||
fruits.max < fruits.current
|
||||
)
|
||||
itemUserNutritionBinding.totalVegetableTv.text = fruits.second.format2String()
|
||||
itemUserNutritionBinding.totalVegetableTv.text = fruits.current.format2String()
|
||||
var fruitsHeight = (fruits.current / columnMax).toFloat()
|
||||
|
||||
if (fruitsHeight == 0F) {
|
||||
itemUserNutritionBinding.divVegetableView.visibility = View.VISIBLE
|
||||
} else {
|
||||
itemUserNutritionBinding.divVegetableView.visibility = View.GONE
|
||||
fruitsHeight += 0.1F
|
||||
}
|
||||
itemUserNutritionBinding.customVegetableColumn.setCustomHeightPercent(
|
||||
(fruits.second / 100).toFloat(),
|
||||
fruitsHeight,
|
||||
true
|
||||
)
|
||||
|
||||
// 设置肉蛋
|
||||
itemUserNutritionBinding.customMeatColumn.setImageDrawable(
|
||||
false,
|
||||
meat.first < meat.second
|
||||
meat.max < meat.current
|
||||
)
|
||||
itemUserNutritionBinding.totalMeatTv.text = meat.second.format2String()
|
||||
itemUserNutritionBinding.totalMeatTv.text = meat.current.format2String()
|
||||
var meatHeight = (meat.current / columnMax).toFloat()
|
||||
if (meatHeight == 0F) {
|
||||
itemUserNutritionBinding.divMeatView.visibility = View.VISIBLE
|
||||
} else {
|
||||
itemUserNutritionBinding.divMeatView.visibility = View.GONE
|
||||
meatHeight += 0.1F
|
||||
}
|
||||
itemUserNutritionBinding.customMeatColumn.setCustomHeightPercent(
|
||||
(meat.second / 100).toFloat(),
|
||||
meatHeight,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopSensorScale() {
|
||||
SensorScaleUtils.closeScale()
|
||||
}
|
||||
|
||||
private fun openCamera(cameraId: String) {
|
||||
private fun openCamera() {
|
||||
Timber.d("openCamera")
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ToastUtils.showToast("无摄像头权限")
|
||||
return
|
||||
}
|
||||
try {
|
||||
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
|
||||
override fun onOpened(device: CameraDevice) {
|
||||
cameraDevice = device
|
||||
// createCameraPreviewSession()
|
||||
val cameraCount = Camera.getNumberOfCameras()
|
||||
if (cameraCount < 3) {
|
||||
ToastUtils.showToast("摄像头数量异常")
|
||||
return
|
||||
}
|
||||
recognizeViewModel.init(
|
||||
PreviewConfig(
|
||||
2, 4, 0, 0
|
||||
2, 1, 90, 90
|
||||
)
|
||||
)
|
||||
initRgbCamera()
|
||||
resumeCamera()
|
||||
if (DualCameraHelper.hasDualCamera() && livenessType === LivenessType.IR) {
|
||||
initIrCamera()
|
||||
}
|
||||
|
||||
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)
|
||||
Timber.e(e, "打开摄像头失败")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,9 +459,12 @@ class SecondaryScreenPresentation(
|
||||
private var isRecognition = false
|
||||
private var rgbCameraHelper: DualCameraHelper? = null
|
||||
private var rgbFaceRectTransformer: FaceRectTransformer? = null
|
||||
private val livenessType = LivenessType.RGB
|
||||
private val livenessType = LivenessType.IR
|
||||
private var openRectInfoDraw = false
|
||||
|
||||
private var irCameraHelper: DualCameraHelper? = null
|
||||
private var irFaceRectTransformer: FaceRectTransformer? = null
|
||||
|
||||
private fun initArcViewModel() {
|
||||
|
||||
recognizeViewModel.setLiveType(livenessType)
|
||||
@@ -404,15 +506,22 @@ class SecondaryScreenPresentation(
|
||||
})
|
||||
recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? ->
|
||||
Timber.i("recognizeNotice observe notice = $notice")
|
||||
if (currentStep == 3 && notice == "leave") {
|
||||
// step1()
|
||||
}
|
||||
})
|
||||
|
||||
recognizeViewModel.recognizeUserId.observe(activity, Observer { userId: String? ->
|
||||
Timber.i("recognizeUserId observe userId = $userId")
|
||||
recognizeViewModel.recognizeUserId.observe(
|
||||
activity,
|
||||
Observer { compareResult: CompareResult ->
|
||||
Timber.i("recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}")
|
||||
recognitionTime = System.currentTimeMillis()
|
||||
recognitionWeight = lastWeight
|
||||
lastFaceTrackId = compareResult.trackId
|
||||
val faceEntity = compareResult.faceEntity
|
||||
val userId = faceEntity.userName
|
||||
if (userId == null) return@Observer
|
||||
viewModel.getUserNutritionData(userId = userId, foodId = currentFood!!.id!!)
|
||||
viewModel.getUserNutritionData(
|
||||
userId = userId,
|
||||
foodId = currentFood!!.id!!
|
||||
)
|
||||
})
|
||||
|
||||
recognizeViewModel.drawRectInfoText.observe(activity, Observer { info ->
|
||||
@@ -427,11 +536,11 @@ class SecondaryScreenPresentation(
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整View的宽高,使预览显示正常且采集框固定为380x380
|
||||
* 调整View的宽高,使预览显示正常且采集框固定为
|
||||
*
|
||||
* @param rgbPreview RGB预览View
|
||||
* @param previewView 显示预览数据的view
|
||||
* @param faceRectView 画框的view (380x380)
|
||||
* @param faceRectView 画框的view
|
||||
* @param previewSize 预览大小
|
||||
* @param displayOrientation 相机旋转角度
|
||||
* @param scale 缩放比例
|
||||
@@ -495,25 +604,38 @@ class SecondaryScreenPresentation(
|
||||
displayOrientation: Int,
|
||||
isMirror: Boolean
|
||||
) {
|
||||
Timber.d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
|
||||
activity.runOnUiThread({
|
||||
val previewSizeRgb = camera.getParameters().getPreviewSize()
|
||||
val layoutParams = adjustPreviewViewSize(
|
||||
binding.dualCameraTexturePreviewRgb,
|
||||
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
|
||||
previewSizeRgb, 90, 1f
|
||||
previewSizeRgb, displayOrientation, 0.6F
|
||||
)
|
||||
Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}")
|
||||
Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}")
|
||||
Timber.d(
|
||||
"initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${
|
||||
ConfigUtil.isDrawRgbRectHorizontalMirror(
|
||||
context
|
||||
)
|
||||
}, isDrawRgbRectVerticalMirror = ${
|
||||
ConfigUtil.isDrawRgbRectVerticalMirror(
|
||||
context
|
||||
)
|
||||
}"
|
||||
)
|
||||
// 调整识别窗口位置
|
||||
rgbFaceRectTransformer = FaceRectTransformer(
|
||||
previewSizeRgb.width,
|
||||
previewSizeRgb.height,
|
||||
layoutParams.width,
|
||||
layoutParams.height,
|
||||
displayOrientation,
|
||||
90,
|
||||
cameraId,
|
||||
isMirror,
|
||||
ConfigUtil.isDrawRgbRectHorizontalMirror(context),
|
||||
ConfigUtil.isDrawRgbRectVerticalMirror(context)
|
||||
true,
|
||||
true
|
||||
)
|
||||
|
||||
recognizeViewModel.onRgbCameraOpened(camera)
|
||||
@@ -536,20 +658,20 @@ class SecondaryScreenPresentation(
|
||||
}
|
||||
|
||||
override fun onCameraClosed() {
|
||||
Timber.i("onCameraClosed: ")
|
||||
Timber.i("initRgbCamera onCameraClosed: ")
|
||||
}
|
||||
|
||||
override fun onCameraError(e: java.lang.Exception) {
|
||||
Timber.i("onCameraError: %s", e.message)
|
||||
Timber.i("initRgbCamera onCameraError: %s", e.message)
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
|
||||
Timber.i("onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
|
||||
Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
|
||||
if (rgbFaceRectTransformer != null) {
|
||||
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
|
||||
}
|
||||
Timber.i("onCameraConfigurationChanged: $cameraID $displayOrientation")
|
||||
Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +684,7 @@ class SecondaryScreenPresentation(
|
||||
)
|
||||
)
|
||||
.rotation(activity.windowManager.defaultDisplay.rotation)
|
||||
.additionalRotation(90) // 角度
|
||||
.additionalRotation(previewConfig.rgbAdditionalDisplayOrientation) // 角度
|
||||
.previewSize(recognizeViewModel.loadPreviewSize())
|
||||
.specificCameraId(previewConfig.rgbCameraId)
|
||||
.isMirror(true)
|
||||
@@ -573,12 +695,96 @@ class SecondaryScreenPresentation(
|
||||
rgbCameraHelper!!.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化红外相机,若活体检测类型是可见光活体检测或不启用活体,则不需要启用
|
||||
*/
|
||||
private fun initIrCamera() {
|
||||
Timber.d("initIrCamera: livenessType = $livenessType")
|
||||
if (livenessType === LivenessType.RGB) {
|
||||
return
|
||||
}
|
||||
val irCameraListener: CameraListener = object : CameraListener {
|
||||
override fun onCameraOpened(
|
||||
camera: Camera,
|
||||
cameraId: Int,
|
||||
displayOrientation: Int,
|
||||
isMirror: Boolean
|
||||
) {
|
||||
Timber.d("initIrCamera IR onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation")
|
||||
val previewSizeIr = camera.getParameters().getPreviewSize()
|
||||
val layoutParams = adjustPreviewViewSize(
|
||||
binding.dualCameraTexturePreviewRgb,
|
||||
binding.dualCameraTexturePreviewIr, binding.dualCameraFaceRectViewIr,
|
||||
previewSizeIr, displayOrientation, 0.25f
|
||||
)
|
||||
|
||||
irFaceRectTransformer = FaceRectTransformer(
|
||||
previewSizeIr.width, previewSizeIr.height,
|
||||
layoutParams.width, layoutParams.height, displayOrientation, cameraId, isMirror,
|
||||
ConfigUtil.isDrawIrRectHorizontalMirror(context),
|
||||
ConfigUtil.isDrawIrRectVerticalMirror(context)
|
||||
)
|
||||
|
||||
recognizeViewModel.onIrCameraOpened(camera)
|
||||
recognizeViewModel.setIrFaceRectTransformer(irFaceRectTransformer)
|
||||
}
|
||||
|
||||
|
||||
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
|
||||
recognizeViewModel.refreshIrPreviewData(nv21)
|
||||
}
|
||||
|
||||
override fun onCameraClosed() {
|
||||
Timber.i("initIrCamera onCameraClosed: ")
|
||||
}
|
||||
|
||||
override fun onCameraError(e: java.lang.Exception) {
|
||||
Timber.i("initIrCamera onCameraError: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
override fun onCameraConfigurationChanged(
|
||||
cameraID: Int,
|
||||
displayOrientation: Int
|
||||
) {
|
||||
if (irFaceRectTransformer != null) {
|
||||
irFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
|
||||
}
|
||||
Timber.i("initIrCamera onCameraConfigurationChanged: cameraID = $cameraID, displayOrientation = $displayOrientation")
|
||||
}
|
||||
}
|
||||
|
||||
val previewConfig = recognizeViewModel.previewConfig
|
||||
irCameraHelper = DualCameraHelper.Builder()
|
||||
.previewViewSize(
|
||||
Point(
|
||||
binding.dualCameraTexturePreviewIr.measuredWidth,
|
||||
binding.dualCameraTexturePreviewIr.measuredHeight
|
||||
)
|
||||
)
|
||||
.rotation(activity.windowManager.defaultDisplay.rotation)
|
||||
.specificCameraId(previewConfig.irCameraId)
|
||||
.previewOn(binding.dualCameraTexturePreviewIr)
|
||||
.cameraListener(irCameraListener)
|
||||
.isMirror(true)
|
||||
.previewSize(recognizeViewModel.loadPreviewSize()) //相机预览大小设置,RGB与IR需使用相同大小
|
||||
.additionalRotation(previewConfig.irAdditionalDisplayOrientation) //额外旋转角度
|
||||
.build()
|
||||
irCameraHelper!!.init()
|
||||
try {
|
||||
irCameraHelper!!.start()
|
||||
} catch (e: RuntimeException) {
|
||||
ToastUtils.showToast(e.message + context.getString(R.string.camera_error_notice))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制RGB、IR画面的实时人脸信息
|
||||
*
|
||||
* @param facePreviewInfoList RGB画面的实时人脸信息
|
||||
*/
|
||||
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
|
||||
// Timber.d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}")
|
||||
if (rgbFaceRectTransformer != null) {
|
||||
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
|
||||
facePreviewInfoList,
|
||||
@@ -588,6 +794,42 @@ class SecondaryScreenPresentation(
|
||||
// 识别成功
|
||||
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
|
||||
}
|
||||
|
||||
if (facePreviewInfoList.isEmpty() || (lastFaceTrackId != facePreviewInfoList[0]!!.trackId)) {
|
||||
if (lastFaceTrackId != -1) {
|
||||
Timber.i("$lastFaceTrackId 用户离开")
|
||||
lastFaceTrackId = -1
|
||||
postUserData()
|
||||
if (mealPickupMode == 0) {
|
||||
step1()
|
||||
} else {
|
||||
step2(currentFood!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun postUserData() {
|
||||
Timber.d("postUserData")
|
||||
if (userNutritionData == null || currentFood == null) {
|
||||
Timber.d("postUserData userNutritionData = ${userNutritionData == null}, currentFood = ${currentFood == null}")
|
||||
return
|
||||
}
|
||||
var eatWeight = 0.0
|
||||
if (mealPickupMode == 0) {//即放即取
|
||||
eatWeight = recognitionWeight - 0
|
||||
} else {
|
||||
eatWeight = recognitionWeight - lastWeight
|
||||
}
|
||||
val userNutritionParam = UserNutritionParam(
|
||||
userId = userNutritionData?.userId!!,
|
||||
foodId = currentFood?.id!!,
|
||||
faceTime = recognitionTime,
|
||||
faceEndTime = System.currentTimeMillis(),
|
||||
eatWeight = eatWeight,//lastWeight
|
||||
foodWeight = lastWeight
|
||||
)
|
||||
viewModel.postUserNutritionData(listOf<UserNutritionParam>(userNutritionParam))
|
||||
}
|
||||
|
||||
fun openRectInfoDraw(view: View?) {
|
||||
@@ -599,6 +841,7 @@ class SecondaryScreenPresentation(
|
||||
override fun onGlobalLayout() {
|
||||
Timber.d("onGlobalLayout")
|
||||
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
|
||||
openCamera()
|
||||
}
|
||||
|
||||
|
||||
@@ -618,13 +861,16 @@ class SecondaryScreenPresentation(
|
||||
rgbCameraHelper!!.release()
|
||||
rgbCameraHelper = null
|
||||
}
|
||||
SensorScaleUtils.closeScale()
|
||||
if (irCameraHelper != null) {
|
||||
irCameraHelper!!.release()
|
||||
irCameraHelper = null
|
||||
}
|
||||
recognizeViewModel.destroy()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
fun resumeCamera() {
|
||||
Timber.d("resumeCamera")
|
||||
Timber.d("resumeCamera isRecognition = $isRecognition")
|
||||
isRecognition = true
|
||||
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
|
||||
rgbCameraHelper!!.start()
|
||||
@@ -632,7 +878,7 @@ class SecondaryScreenPresentation(
|
||||
}
|
||||
|
||||
fun pauseCamera() {
|
||||
Timber.d("pauseCamera")
|
||||
Timber.d("pauseCamera isRecognition = $isRecognition")
|
||||
isRecognition = false
|
||||
|
||||
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
|
||||
|
||||
@@ -2,9 +2,15 @@ package com.sw.dualscreen.presentation
|
||||
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.sw.dualscreen.ext.toSafeDouble
|
||||
import com.sw.dualscreen.model.response.FoodInfo
|
||||
import com.sw.dualscreen.model.response.UserNutritionData
|
||||
import timber.log.Timber
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* 用户营养信息计算工具
|
||||
*/
|
||||
object UserNutritionUtils {
|
||||
|
||||
/**
|
||||
@@ -16,46 +22,68 @@ object UserNutritionUtils {
|
||||
weight: Double,
|
||||
dinnerType: String
|
||||
): CalcResultInfo {
|
||||
// 1. 初始化变量
|
||||
|
||||
// 初始化变量
|
||||
var foodKcal = 0.0
|
||||
var totalKcal = 0.0
|
||||
var (vegetable, meat, fruits, grain) = List(4) { 0.0 }
|
||||
|
||||
// 2. 处理食物信息
|
||||
// 处理食物信息
|
||||
foodKcal = calculateValue(foodInfo.stFoodInfoMaterial?.energyKcal, weight)
|
||||
|
||||
Timber.d("calculateNutrition foodKcal = ${foodKcal}, energyKcal = ${foodInfo.stFoodInfoMaterial?.energyKcal}, weight = $weight")
|
||||
grain = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.grainValue(), weight)
|
||||
fruits = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.fruitsValue(), weight)
|
||||
vegetable = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.vegetableValue(), weight)
|
||||
meat = calculateValue(foodInfo.stFoodInfoPagodaAPPVO?.meatValue(), weight)
|
||||
|
||||
// 3. 合并用户数据
|
||||
// 合并用户数据
|
||||
grain += userModel.stFoodInfoPagoda?.grainValue() ?: 0.0
|
||||
|
||||
fruits += userModel.stFoodInfoPagoda?.fruitsValue() ?: 0.0
|
||||
vegetable = vegetable.plus(userModel.stFoodInfoPagoda?.vegetableValue() ?: 0.0)
|
||||
meat += userModel.stFoodInfoPagoda?.meatValue() ?: 0.0
|
||||
|
||||
totalKcal = foodKcal + userModel.energyValue()
|
||||
val fruitsAndVeg = fruits + vegetable
|
||||
|
||||
// 4. 判断当餐最大热量
|
||||
Timber.d("calculateNutrition totalKcal = ${totalKcal}, energyValue = ${userModel.energyValue()}")
|
||||
if (totalKcal < 0){
|
||||
totalKcal = 0.0
|
||||
}
|
||||
// 判断当餐最大热量
|
||||
val maxKcal =
|
||||
userModel.totalEnergyCalculateScoreValue() * calculateDinnerTypeRatio(dinnerType) / 10
|
||||
|
||||
Timber.d("calculateNutrition maxKcal = ${maxKcal}, CalculateScoreValue = ${userModel.totalEnergyCalculateScoreValue()}")
|
||||
val pagoda = userModel.stFoodInfoPagoda
|
||||
val fruitsInfo = parseRecommend(pagoda?.fruitsRecommend, fruits)
|
||||
val vegetableInfo = parseRecommend(pagoda?.vegetableRecommend, vegetable)
|
||||
val calcResult = CalcResultInfo(
|
||||
totalKcal = maxKcal to totalKcal,
|
||||
grain = parseMax(pagoda?.grainRecommend) to grain,
|
||||
fruits = parseMax(pagoda?.fruitsRecommend) + parseMax(pagoda?.vegetableRecommend) to fruitsAndVeg,
|
||||
meat = parseMax(pagoda?.meatRecommend) to meat
|
||||
totalKcal = CalcInfo(
|
||||
max = maxKcal,
|
||||
min = 0.0,
|
||||
current = totalKcal
|
||||
),
|
||||
grain = parseRecommend(pagoda?.grainRecommend, grain),
|
||||
fruits = CalcInfo( // 果蔬 = 水果+蔬菜
|
||||
max = fruitsInfo.max + vegetableInfo.max,
|
||||
min = fruitsInfo.min + vegetableInfo.min,
|
||||
current = fruitsInfo.current + vegetableInfo.current
|
||||
),
|
||||
meat = parseRecommend(pagoda?.meatRecommend, meat)
|
||||
)
|
||||
return calcResult
|
||||
}
|
||||
|
||||
fun parseMax(recommend: String?): Double {
|
||||
if (TextUtils.isEmpty(recommend) || !recommend!!.contains("-")) return 0.0
|
||||
fun parseRecommend(recommend: String?, current: Double): CalcInfo {
|
||||
var newCurrent = if (current < 0) 0.0 else current
|
||||
if (TextUtils.isEmpty(recommend) || !recommend!!.contains("-")) {
|
||||
Timber.e("parseRecommend recommend 格式错误")
|
||||
return CalcInfo(
|
||||
newCurrent,
|
||||
0.0,
|
||||
newCurrent
|
||||
)
|
||||
}
|
||||
val (min, max) = recommend.split("-")
|
||||
return max.toDouble()
|
||||
return CalcInfo(max = max.toSafeDouble(), min = min.toSafeDouble(), current = newCurrent)
|
||||
}
|
||||
|
||||
private fun calculateDinnerTypeRatio(dinnerType: String): Int {
|
||||
@@ -66,14 +94,28 @@ object UserNutritionUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算每百克含量
|
||||
*/
|
||||
private fun calculateValue(nutrient: Double?, weight: Double): Double {
|
||||
return weight * (nutrient ?: 1.0)
|
||||
return weight * (nutrient ?: 1.0) / 100
|
||||
}
|
||||
|
||||
fun getMaxInt(calcResultInfo: CalcResultInfo): Int {
|
||||
val max1 = max(calcResultInfo.grain.max, calcResultInfo.fruits.max)
|
||||
return max(max1, calcResultInfo.meat.max).toInt()
|
||||
}
|
||||
|
||||
data class CalcInfo(
|
||||
var max: Double,
|
||||
var min: Double,
|
||||
var current: Double,
|
||||
)
|
||||
|
||||
data class CalcResultInfo(
|
||||
var totalKcal: Pair<Double, Double>,
|
||||
var grain: Pair<Double, Double>,
|
||||
var fruits: Pair<Double, Double>,
|
||||
var meat: Pair<Double, Double>,
|
||||
var totalKcal: CalcInfo,
|
||||
var grain: CalcInfo,
|
||||
var fruits: CalcInfo,
|
||||
var meat: CalcInfo,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.sw.dualscreen.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.sw.dualscreen.activity.InitActivity
|
||||
import timber.log.Timber
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
|
||||
Timber.d("设备启动完成,开始执行自启动逻辑")
|
||||
val intent = Intent(context, InitActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.sw.dualscreen.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.sw.dualscreen.GlobalData
|
||||
import com.sw.dualscreen.model.request.UserNutritionParam
|
||||
import com.sw.dualscreen.model.response.ApiResponse
|
||||
@@ -9,6 +11,9 @@ import com.sw.dualscreen.model.response.FoodInfo
|
||||
import com.sw.dualscreen.model.response.UserFaceInfo
|
||||
import com.sw.dualscreen.model.response.UserNutritionData
|
||||
import com.sw.dualscreen.network.api.ApiService
|
||||
import com.sw.dualscreen.utils.ImageUtil
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
/**
|
||||
* 远程数据处理
|
||||
@@ -99,8 +104,43 @@ class RemoteRepository constructor(
|
||||
* 提交就餐数据
|
||||
*/
|
||||
suspend fun postUserNutritionData(
|
||||
param: UserNutritionParam
|
||||
param: List<UserNutritionParam>
|
||||
): ApiResponse<String> {
|
||||
return safeApiCall { apiService.postUserNutritionData(param = param) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜品信息
|
||||
* @param restId 从device服务获取的canteenId字段
|
||||
* @param foodName 菜品名称,多个使用逗号拼接
|
||||
*/
|
||||
suspend fun getFoodInfo(
|
||||
restId: String = GlobalData.restId,
|
||||
foodName: String,
|
||||
): ApiResponse<List<FoodInfo>> {
|
||||
return safeApiCall { apiService.getFoodInfo(restId = restId, foodName = foodName) }
|
||||
}
|
||||
/**
|
||||
* 获取菜品信息
|
||||
* @param restId 从device服务获取的canteenId字段
|
||||
* @param foodName 菜品名称,多个使用逗号拼接
|
||||
*/
|
||||
suspend fun postImageData(
|
||||
context: Context,
|
||||
restId: String = GlobalData.restId,
|
||||
foodId: String,
|
||||
foodName: String,
|
||||
foodVector: String,
|
||||
uri: Uri,
|
||||
): ApiResponse<String> {
|
||||
|
||||
val params = HashMap<String, RequestBody>()
|
||||
params["placeId"] = restId.toRequestBody()
|
||||
params["foodId"] = foodId.toRequestBody()
|
||||
params["foodName"] = foodName.toRequestBody()
|
||||
params["foodVector"] = foodVector.toRequestBody()
|
||||
val imagePart = ImageUtil.uriToMultipart(context, uri, "foodPic")
|
||||
|
||||
return safeApiCall { apiService.postImageData(params = params, image = imagePart) }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sw.dualscreen.sdk
|
||||
|
||||
import android.util.Log
|
||||
import com.sw.plate.utils.ToastUtils
|
||||
import com.wabon.wbintelligenthardwaresdk.api.SensorScale
|
||||
import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult
|
||||
@@ -19,7 +18,10 @@ object SensorScaleUtils {
|
||||
const val baudRate = 115200
|
||||
private var mSensorScale: SensorScale? = null
|
||||
private var isOpened: Boolean = false
|
||||
private var isRead: Boolean = false
|
||||
var isZero = false
|
||||
private var callback: Callback? = {}
|
||||
private var lastWeight: Double? = null
|
||||
|
||||
private fun init() {
|
||||
mSensorScale = SensorScale(object : OnScaleResult {
|
||||
@@ -35,7 +37,8 @@ object SensorScaleUtils {
|
||||
}
|
||||
// Log.d(TAG, "readWeight state = ${stateStr}, weight = $value")
|
||||
// 只使用稳定值
|
||||
if (state == SensorScale.STATE_STABLE) {
|
||||
if (state == SensorScale.STATE_STABLE && lastWeight != value) {
|
||||
lastWeight = value
|
||||
callback?.invoke(value)
|
||||
}
|
||||
}
|
||||
@@ -44,11 +47,11 @@ object SensorScaleUtils {
|
||||
* 读取鉴别率
|
||||
*/
|
||||
override fun readIdentify(rate: Int) {
|
||||
Log.e(TAG, "readIdentify rate = $rate")
|
||||
Timber.e("readIdentify rate = $rate")
|
||||
}
|
||||
|
||||
override fun fail(errCode: Int) {
|
||||
Log.e(TAG, "fail code = $errCode")
|
||||
Timber.e("fail code = $errCode")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -58,25 +61,27 @@ object SensorScaleUtils {
|
||||
* @param autoScale 是否开启自动读取
|
||||
*/
|
||||
fun startScale(autoScale: Boolean = true, callback: Callback?) {
|
||||
Timber.d("startScale isOpened = $isOpened")
|
||||
if (isOpened) {
|
||||
startContinuousRead(callback = callback)
|
||||
return
|
||||
}
|
||||
this.callback = callback
|
||||
init()
|
||||
mSensorScale?.zero {
|
||||
Timber.d("startScale: zero")
|
||||
}
|
||||
mSensorScale?.openScale(serialPort, baudRate) { open ->
|
||||
isOpened = open
|
||||
Timber.d("isOpened = $isOpened")
|
||||
if (open) {
|
||||
// zero()
|
||||
if (autoScale) {
|
||||
Thread({
|
||||
Thread {
|
||||
Thread.sleep(1000)
|
||||
// 打开后需要等待后才能调用,否则会 1001 SDK未初始化
|
||||
mSensorScale?.startContinuousRead()
|
||||
}).start()
|
||||
// Thread.sleep(1000)
|
||||
zero()
|
||||
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,23 +91,24 @@ object SensorScaleUtils {
|
||||
* 开启自动读取重量
|
||||
*/
|
||||
fun startContinuousRead(callback: Callback?) {
|
||||
this.callback = callback
|
||||
Log.d(TAG, "开启自动读取 = $isOpened")
|
||||
if (!isOpened) {
|
||||
Timber.d("开启自动读取 = $isRead")
|
||||
if (isRead) {
|
||||
return
|
||||
}
|
||||
this.callback = callback
|
||||
mSensorScale?.startContinuousRead()
|
||||
isRead = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动读取重量
|
||||
*/
|
||||
fun readWeight(callback: Callback?) {
|
||||
this.callback = callback
|
||||
Log.d(TAG, "isOpened = $isOpened")
|
||||
Timber.d("isOpened = $isOpened")
|
||||
if (!isOpened) {
|
||||
return
|
||||
}
|
||||
this.callback = callback
|
||||
mSensorScale?.readWeight()
|
||||
}
|
||||
|
||||
@@ -110,9 +116,11 @@ object SensorScaleUtils {
|
||||
* 零位标定
|
||||
*/
|
||||
fun zero() {
|
||||
Timber.d("zero isOpened = $isOpened, mSensorScale = $mSensorScale")
|
||||
if (!isOpened) return
|
||||
mSensorScale?.zero {
|
||||
Log.d(TAG, "零位标定操作成功")
|
||||
isZero = true
|
||||
Timber.d("zero 零位标定操作成功")
|
||||
ToastUtils.showToast("零位标定操作成功")
|
||||
}
|
||||
}
|
||||
@@ -123,7 +131,7 @@ object SensorScaleUtils {
|
||||
fun tare() {
|
||||
if (!isOpened) return
|
||||
mSensorScale?.tare {
|
||||
Log.d(TAG, "去皮置零操作成功")
|
||||
Timber.d("去皮置零操作成功")
|
||||
ToastUtils.showToast("去皮置零操作成功")
|
||||
}
|
||||
}
|
||||
@@ -132,18 +140,19 @@ object SensorScaleUtils {
|
||||
* 停止自动读取重量
|
||||
*/
|
||||
fun stopContinuousRead() {
|
||||
Log.d(TAG, "isOpened = $isOpened")
|
||||
Timber.d("isOpened = $isOpened")
|
||||
if (!isOpened) {
|
||||
return
|
||||
}
|
||||
mSensorScale?.stopContinuousRead()
|
||||
isRead = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭称重
|
||||
*/
|
||||
fun closeScale() {
|
||||
Log.d(TAG, "closeScale")
|
||||
Timber.d("closeScale")
|
||||
isOpened = false
|
||||
mSensorScale?.closeScale()
|
||||
mSensorScale == null
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
|
||||
|
||||
object AssetsTool {
|
||||
|
||||
// fun readJson(context: Context, fileName: String): List<String> {
|
||||
// val json = readAssetsFile(context, fileName)
|
||||
// val json2 = json.replace("[[", "[").replaceAfterLast("]]", "]")
|
||||
// val list = json2.split("],")
|
||||
// val resultList = mutableListOf<String>()
|
||||
// var count = 0
|
||||
// val tempList = mutableListOf<String>()
|
||||
// list.forEachIndexed { index, text ->
|
||||
// if (count >= 500) {
|
||||
// val tempJson = "[${tempList.joinToString (",")}]"
|
||||
// resultList.add(tempJson)
|
||||
// tempList.clear()
|
||||
// count = 0
|
||||
// }
|
||||
// val newText = if (index == list.size - 1) text else "${text}]"
|
||||
// tempList.add(newText)
|
||||
// count++
|
||||
// }
|
||||
// if (count < 500) {
|
||||
// val tempJson = "[${tempList.joinToString(",")}]"
|
||||
// resultList.add(tempJson)
|
||||
// }
|
||||
// return resultList
|
||||
// }
|
||||
|
||||
fun readAssetsFile(context: Context, fileName: String): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
try {
|
||||
val bf = BufferedReader(InputStreamReader(context.assets.open(fileName)))
|
||||
bf.useLines { lines -> lines.forEach { stringBuilder.append(it) } }
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
fun loadImagesFromAssets(context: Context, subPath: String): MutableList<Bitmap> {
|
||||
val bitmaps: MutableList<Bitmap> = mutableListOf()
|
||||
val assetManager = context.assets
|
||||
|
||||
try {
|
||||
val files = assetManager.list(subPath)
|
||||
files?.forEach { file ->
|
||||
assetManager.open("$subPath/$file").use { `is` ->
|
||||
val bitmap = BitmapFactory.decodeStream(`is`)
|
||||
if (bitmap != null) {
|
||||
bitmaps.add(bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return bitmaps
|
||||
}
|
||||
|
||||
fun loadImageBitmapFromAssets(context: Context, imagePath:String, action:(bmp: Bitmap)-> Unit) {
|
||||
context.assets.open(imagePath).use { `is` ->
|
||||
val bitmap = BitmapFactory.decodeStream(`is`)
|
||||
action(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.sw.dualscreen.utils
|
||||
import android.graphics.Bitmap
|
||||
|
||||
object BitmapCropper {
|
||||
/**
|
||||
* 裁剪Bitmap中心区域为指定尺寸
|
||||
* @param original 原始Bitmap
|
||||
* @param targetWidth 目标宽度
|
||||
* @param targetHeight 目标高度
|
||||
* @return 裁剪后的Bitmap
|
||||
*/
|
||||
fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
|
||||
val originalWidth = original.width
|
||||
val originalHeight = original.height
|
||||
|
||||
// 计算中心点坐标
|
||||
var startX = (originalWidth - targetWidth) / 2
|
||||
var startY = (originalHeight - targetHeight) / 2
|
||||
|
||||
// 边界检查
|
||||
startX = startX.coerceAtLeast(0)
|
||||
startY = startY.coerceAtLeast(0)
|
||||
val actualWidth = minOf(targetWidth, originalWidth - startX)
|
||||
val actualHeight = minOf(targetHeight, originalHeight - startY)
|
||||
|
||||
return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
fun main() {
|
||||
// 假设这是从资源加载的Bitmap
|
||||
val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
|
||||
|
||||
// 裁剪中心500x500区域
|
||||
val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500)
|
||||
println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}")
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.sw.dualscreen.utils
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Environment
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
object BitmapSaver {
|
||||
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
|
||||
fun saveToPublicDirectory(
|
||||
bitmap: Bitmap,
|
||||
folderName: String = Environment.DIRECTORY_PICTURES,
|
||||
fileName: String,
|
||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
|
||||
quality: Int = 100
|
||||
): File? {
|
||||
val dir = Environment.getExternalStoragePublicDirectory(folderName)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
|
||||
return saveBitmap(bitmap, File(dir, fileName), format, quality)
|
||||
}
|
||||
|
||||
// 保存到应用私有目录(无需权限)
|
||||
fun saveToAppFilesDir(
|
||||
bitmap: Bitmap,
|
||||
context: Context,
|
||||
fileName: String,
|
||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||
quality: Int = 100
|
||||
): File? {
|
||||
//val dir = context.getExternalFilesDir(null)
|
||||
val dir = File(context.cacheDir, "crop")
|
||||
if (dir.exists().not()) {
|
||||
dir.mkdirs()
|
||||
}
|
||||
return saveBitmap(bitmap, File(dir, fileName), format, quality)
|
||||
}
|
||||
|
||||
private fun saveBitmap(
|
||||
bitmap: Bitmap,
|
||||
outputFile: File,
|
||||
format: Bitmap.CompressFormat,
|
||||
quality: Int
|
||||
): File? {
|
||||
return try {
|
||||
FileOutputStream(outputFile).use { fos ->
|
||||
bitmap.compress(format, quality, fos)
|
||||
fos.flush()
|
||||
}
|
||||
outputFile
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/**
|
||||
* 简洁相机工具类
|
||||
* 调用系统相机拍照并返回照片路径
|
||||
*/
|
||||
class CameraHelper(
|
||||
private val context: Context,
|
||||
caller: ActivityResultCaller,
|
||||
private val authority: String,
|
||||
private val onResult: (uri: Uri, filePath: String?) -> Unit
|
||||
) {
|
||||
|
||||
private lateinit var imageUri: Uri
|
||||
private val takePictureLauncher: ActivityResultLauncher<Uri>
|
||||
|
||||
init {
|
||||
takePictureLauncher =
|
||||
caller.registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||
if (success) {
|
||||
val path = getRealPathFromUri(context, imageUri)
|
||||
onResult(imageUri, path)
|
||||
} else {
|
||||
Toast.makeText(context, "拍照取消", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开系统相机 */
|
||||
fun openCamera() {
|
||||
// 1. 创建存储图片的文件
|
||||
val photoFile = File(context.externalCacheDir, "IMG_${System.currentTimeMillis()}.jpg")
|
||||
photoFile.parentFile?.mkdirs() // 确保目录存在
|
||||
|
||||
// 2. 生成 content:// Uri
|
||||
imageUri = FileProvider.getUriForFile(context, authority, photoFile)
|
||||
|
||||
// 3. 启动相机
|
||||
takePictureLauncher.launch(imageUri)
|
||||
}
|
||||
|
||||
/** 可选:将 content:// 转为实际路径 */
|
||||
private fun getRealPathFromUri(context: Context, uri: Uri): String? {
|
||||
var filePath: String? = null
|
||||
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
val fileName = cursor.getString(nameIndex)
|
||||
val file = File(context.cacheDir, fileName)
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
FileOutputStream(file).use { output -> input.copyTo(output) }
|
||||
}
|
||||
filePath = file.absolutePath
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.view.CameraController
|
||||
import androidx.camera.view.LifecycleCameraController
|
||||
import androidx.camera.view.PreviewView
|
||||
|
||||
class CameraUtils(private var activity: ComponentActivity) {
|
||||
|
||||
private var cameraController: LifecycleCameraController? = null
|
||||
private var photoCaptureHelper: PhotoCaptureHelper? = null
|
||||
|
||||
// private var isCameraReady = false
|
||||
fun takePhoto(callback: (Uri) -> Unit) {
|
||||
cameraController?.let {
|
||||
if (photoCaptureHelper == null) {
|
||||
initCaptureHelper()
|
||||
}
|
||||
}
|
||||
photoCaptureHelper?.let {
|
||||
it.addSuccessCallback(callback)
|
||||
it.bindCameraCallback {
|
||||
bind()
|
||||
}
|
||||
it.takePhoto()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initCaptureHelper() {
|
||||
photoCaptureHelper = PhotoCaptureHelper(
|
||||
context = activity,
|
||||
cameraController = cameraController!!,
|
||||
onSuccess = {},
|
||||
onError = { msg ->
|
||||
//toast(msg)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun setPreviewController(previewView: PreviewView?) {
|
||||
if (previewView?.controller == null) {
|
||||
previewView?.controller = cameraController
|
||||
}
|
||||
}
|
||||
|
||||
fun initCamera() {
|
||||
if (cameraController == null) {
|
||||
cameraController = LifecycleCameraController(activity).apply {
|
||||
// 必须设置有效的用例
|
||||
setEnabledUseCases(
|
||||
CameraController.IMAGE_CAPTURE
|
||||
// or CameraController.VIDEO_CAPTURE
|
||||
)
|
||||
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
}
|
||||
bind()
|
||||
}
|
||||
|
||||
// if (isCameraReady.not()) {
|
||||
// try {
|
||||
// cameraController!!.initializationFuture.addListener({
|
||||
// isCameraReady = true
|
||||
// Timber.d("Camera initialized successfully")
|
||||
// }, ContextCompat.getMainExecutor(this))
|
||||
// } catch (e: Exception) {
|
||||
// Timber.d("Camera initialized error = ${e.message}")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fun bind() {
|
||||
cameraController?.bindToLifecycle(activity)
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
cameraController?.unbind()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Process
|
||||
import com.sw.dualscreen.activity.MainActivity
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.apply
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.io.bufferedReader
|
||||
import kotlin.io.readLines
|
||||
import kotlin.io.use
|
||||
import kotlin.jvm.java
|
||||
import kotlin.run
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.text.endsWith
|
||||
import kotlin.text.toByteArray
|
||||
|
||||
/**
|
||||
* 崩溃处理
|
||||
*/
|
||||
class CrashHandler private constructor(private val context: Context) :
|
||||
Thread.UncaughtExceptionHandler {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CrashHandler"
|
||||
private const val CRASH_REPORTS_DIR = "crash_reports"
|
||||
private const val LOG_LINES = 500 // 收集最近500行日志
|
||||
|
||||
@Volatile
|
||||
private var instance: CrashHandler? = null
|
||||
|
||||
fun init(context: Context) {
|
||||
if (instance == null) {
|
||||
synchronized(CrashHandler::class.java) {
|
||||
if (instance == null) {
|
||||
instance = CrashHandler(context.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCrashReportFiles(context: Context): Array<File> {
|
||||
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
|
||||
return if (crashDir.exists() && crashDir.isDirectory) {
|
||||
crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
|
||||
} else {
|
||||
emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCrashReports(context: Context) {
|
||||
getCrashReportFiles(context).forEach { it.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
init {
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
}
|
||||
|
||||
override fun uncaughtException(thread: Thread, ex: Throwable) {
|
||||
handleException(thread, ex)
|
||||
|
||||
// 如果系统提供了默认的异常处理器,则交给系统去结束程序
|
||||
// 否则自己结束程序
|
||||
defaultHandler?.uncaughtException(thread, ex) ?: run {
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动重启app
|
||||
*/
|
||||
private fun restartApp() {
|
||||
// 延迟1秒后重启应用
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context, 0, intent,
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
|
||||
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(1)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private fun handleException(thread: Thread, ex: Throwable) {
|
||||
// 收集设备信息和异常信息
|
||||
val crashInfo = collectCrashInfo(thread, ex)
|
||||
|
||||
// 保存日志文件
|
||||
saveCrashInfoToFile(crashInfo)
|
||||
|
||||
// 这里可以添加其他处理逻辑,比如上传到服务器等
|
||||
}
|
||||
|
||||
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
|
||||
return buildString {
|
||||
// 收集设备信息
|
||||
collectDeviceInfo(this)
|
||||
|
||||
// 收集应用日志
|
||||
append("\n\n").append(collectLogs())
|
||||
|
||||
// 收集线程和异常信息
|
||||
append("\n\n========== Thread & Exception Info ==========\n")
|
||||
append("Thread: ${thread.name}\n")
|
||||
append("Stack Trace:\n")
|
||||
|
||||
val sw = StringWriter()
|
||||
val pw = PrintWriter(sw)
|
||||
ex.printStackTrace(pw)
|
||||
var cause: Throwable? = ex.cause
|
||||
while (cause != null) {
|
||||
cause.printStackTrace(pw)
|
||||
cause = cause.cause
|
||||
}
|
||||
pw.close()
|
||||
append(sw.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectDeviceInfo(sb: StringBuilder) {
|
||||
sb.append("========== Device Info ==========\n")
|
||||
try {
|
||||
// 应用信息
|
||||
val pm = context.packageManager
|
||||
val pi = pm.getPackageInfo(context.packageName, 0)
|
||||
|
||||
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
|
||||
|
||||
// Android 版本信息
|
||||
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
|
||||
|
||||
// 设备信息
|
||||
sb.append("Vendor: ${Build.MANUFACTURER}\n")
|
||||
sb.append("Model: ${Build.MODEL}\n")
|
||||
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
|
||||
|
||||
// 其他信息
|
||||
sb.append("Locale: ${Locale.getDefault()}\n")
|
||||
sb.append(
|
||||
"Current Time: ${
|
||||
SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
Locale.getDefault()
|
||||
).format(Date())
|
||||
}\n"
|
||||
)
|
||||
|
||||
// 内存信息
|
||||
val memoryInfo = ActivityManager.MemoryInfo()
|
||||
val activityManager =
|
||||
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
activityManager.getMemoryInfo(memoryInfo)
|
||||
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
|
||||
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
|
||||
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while collecting device info")
|
||||
sb.append("Error while collecting device info: ${e.message}\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectLogs(): String {
|
||||
return buildString {
|
||||
append("========== Application Logs ==========\n")
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
|
||||
val reader = process.inputStream.bufferedReader()
|
||||
|
||||
val logLines = reader.readLines()
|
||||
val start = kotlin.comparisons.maxOf(0, logLines.size - LOG_LINES)
|
||||
|
||||
logLines.subList(start, logLines.size).forEach {
|
||||
append(it).append("\n")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "Error collecting logs")
|
||||
append("Error collecting logs: ${e.message}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveCrashInfoToFile(crashInfo: String) {
|
||||
try {
|
||||
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
|
||||
val fileName = "crash_$time.log"
|
||||
|
||||
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
|
||||
if (!crashDir.exists() && !crashDir.mkdirs()) {
|
||||
Timber.tag(TAG).e("Failed to create crash report directory")
|
||||
return
|
||||
}
|
||||
|
||||
val crashFile = File(crashDir, fileName)
|
||||
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
|
||||
|
||||
Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error saving crash info to file")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧的崩溃日志
|
||||
*/
|
||||
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
|
||||
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
|
||||
if (!crashDir.exists() || !crashDir.isDirectory) return
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
|
||||
|
||||
crashDir.listFiles()?.forEach { file ->
|
||||
if (file.lastModified() < now - maxAgeMillis) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
class Debouncer(private val delayMillis: Long) {
|
||||
/**
|
||||
* 防抖工具
|
||||
*/
|
||||
class Debouncer(private val delayMillis: Long = 2000) {
|
||||
private var lastActionTime = 0L
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
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,47 @@
|
||||
package com.example.utils
|
||||
|
||||
import android.util.Base64
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* 工具类:float[] 与 Base64 字符串互转
|
||||
* 无需外部依赖,适用于 Android 原生项目。
|
||||
*/
|
||||
object FloatBase64Utils {
|
||||
|
||||
/**
|
||||
* float数组 → Base64字符串
|
||||
* @param values FloatArray
|
||||
* @return Base64字符串(无换行)
|
||||
*/
|
||||
fun floatArrayToBase64(values: FloatArray): String {
|
||||
if (values.isEmpty()) return ""
|
||||
|
||||
val buffer = ByteBuffer.allocate(values.size * 4)
|
||||
buffer.order(ByteOrder.BIG_ENDIAN) // 默认大端序,可改 LITTLE_ENDIAN
|
||||
values.forEach { buffer.putFloat(it) }
|
||||
|
||||
val bytes = buffer.array()
|
||||
return Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64字符串 → float数组
|
||||
* @param base64 Base64字符串
|
||||
* @return FloatArray
|
||||
*/
|
||||
fun base64ToFloatArray(base64: String?): FloatArray {
|
||||
if (base64.isNullOrEmpty()) return FloatArray(0)
|
||||
|
||||
val bytes = Base64.decode(base64, Base64.NO_WRAP)
|
||||
val buffer = ByteBuffer.wrap(bytes)
|
||||
buffer.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
val result = FloatArray(bytes.size / 4)
|
||||
for (i in result.indices) {
|
||||
result[i] = buffer.getFloat()
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
object ImageUtil {
|
||||
|
||||
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
|
||||
return try {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
BitmapFactory.decodeStream(stream)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun uriToMultipart(context: Context, uri: Uri, partName: String): MultipartBody.Part? {
|
||||
try {
|
||||
val contentResolver = context.contentResolver
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||
|
||||
// 临时文件(避免直接访问SAF文件)
|
||||
val tempFile = File.createTempFile("upload_", ".jpg", context.cacheDir)
|
||||
|
||||
val outputStream = FileOutputStream(tempFile)
|
||||
inputStream.copyTo(outputStream)
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
|
||||
val requestFile = RequestBody.create("image/*".toMediaTypeOrNull(), tempFile)
|
||||
return MultipartBody.Part.createFormData(partName, tempFile.name, requestFile)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
|
||||
/**
|
||||
* 键盘工具类
|
||||
*/
|
||||
object KeyboardUtils {
|
||||
|
||||
/**
|
||||
* 显示键盘
|
||||
*/
|
||||
fun showKeyboard(editText: EditText) {
|
||||
editText.requestFocus()
|
||||
val imm = editText.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏键盘
|
||||
*/
|
||||
fun hideKeyboard(activity: Activity) {
|
||||
val view = activity.currentFocus ?: activity.window.decorView
|
||||
hideKeyboard(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏键盘
|
||||
*/
|
||||
fun hideKeyboard(view: View) {
|
||||
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 为EditText设置EditorAction监听并隐藏键盘
|
||||
*/
|
||||
fun setupEditorAction(editText: EditText, onAction: (() -> Unit)? = null) {
|
||||
editText.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE ||
|
||||
actionId == EditorInfo.IME_ACTION_SEARCH ||
|
||||
actionId == EditorInfo.IME_ACTION_GO ||
|
||||
actionId == EditorInfo.IME_ACTION_SEND ||
|
||||
actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
|
||||
hideKeyboard(editText)
|
||||
onAction?.invoke()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
import androidx.camera.view.CameraController
|
||||
import androidx.core.content.ContextCompat
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 拍照工具类
|
||||
* @param context Context 上下文
|
||||
* @param cameraController CameraController 相机控制器
|
||||
* @param onSuccess (Uri) -> Unit 拍照成功回调
|
||||
* @param onError (String) -> Unit 拍照失败回调
|
||||
*/
|
||||
class PhotoCaptureHelper(
|
||||
private val context: Context,
|
||||
private val cameraController: CameraController,
|
||||
private val onSuccess: (Uri) -> Unit = {},
|
||||
private val onError: (String) -> Unit = {}
|
||||
) {
|
||||
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
|
||||
fun addSuccessCallback(callback:(Uri) -> Unit) {
|
||||
if (callbackList.contains(callback).not()) {
|
||||
callbackList.add(callback)
|
||||
}
|
||||
}
|
||||
|
||||
private var bindCamera:(()->Unit)?=null
|
||||
fun bindCameraCallback(callback:()->Unit) {
|
||||
this.bindCamera = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* 拍照方法
|
||||
* @param fileNamePrefix 文件名前缀,默认为"IMG_"
|
||||
* @param fileExtension 文件扩展名,默认为".jpg"
|
||||
*/
|
||||
fun takePhoto(
|
||||
fileNamePrefix: String = "IMG_",
|
||||
fileExtension: String = ".jpg"
|
||||
) {
|
||||
Timber.d("开始拍照采集")
|
||||
|
||||
try {
|
||||
val executor = ContextCompat.getMainExecutor(context)
|
||||
val cacheDir = context.cacheDir
|
||||
val photoFile = File.createTempFile(
|
||||
"${fileNamePrefix}${System.currentTimeMillis()}",
|
||||
fileExtension,
|
||||
cacheDir
|
||||
)
|
||||
|
||||
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
|
||||
|
||||
cameraController.takePicture(
|
||||
outputOptions,
|
||||
executor,
|
||||
object : ImageCapture.OnImageSavedCallback {
|
||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
|
||||
Timber.d("照片保存成功: $photoUri")
|
||||
onSuccess(photoUri)
|
||||
callbackList.forEach {
|
||||
it(photoUri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
val errorMsg = "拍照失败: ${exception.message}"
|
||||
if (errorMsg.contains("Not bound to a valid Camera")) {
|
||||
if (bindCamera != null) {
|
||||
bindCamera?.invoke()
|
||||
//takePhoto()
|
||||
}
|
||||
|
||||
}
|
||||
Timber.e(exception, errorMsg)
|
||||
onError(errorMsg)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
val errorMsg = "创建临时文件失败: ${e.message}"
|
||||
Timber.e(e, errorMsg)
|
||||
onError(errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
|
||||
/**
|
||||
* 二维码生成工具类
|
||||
*/
|
||||
object QRCodeUtil {
|
||||
|
||||
/**
|
||||
* 生成二维码(默认大小)
|
||||
* @param content 二维码内容
|
||||
* @return 生成的二维码Bitmap
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun generateQRCode(content: String, size: Int = 500): Bitmap? {
|
||||
return generateQRCode(content, size, Color.BLACK, Color.WHITE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码(自定义颜色)
|
||||
* @param content 二维码内容
|
||||
* @param size 二维码边长(像素)
|
||||
* @param colorCode 二维码颜色
|
||||
* @param backgroundColor 背景颜色
|
||||
* @return 生成的二维码Bitmap
|
||||
*/
|
||||
fun generateQRCode(
|
||||
content: String,
|
||||
size: Int,
|
||||
colorCode: Int,
|
||||
backgroundColor: Int
|
||||
): Bitmap? {
|
||||
if (content.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
val hints = mutableMapOf<EncodeHintType, Any>().apply {
|
||||
put(EncodeHintType.CHARACTER_SET, "UTF-8")
|
||||
put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H) // 纠错级别
|
||||
put(EncodeHintType.MARGIN, 1) // 边距
|
||||
}
|
||||
|
||||
val bitMatrix = QRCodeWriter().encode(
|
||||
content,
|
||||
BarcodeFormat.QR_CODE,
|
||||
size,
|
||||
size,
|
||||
hints
|
||||
)
|
||||
|
||||
val pixels = IntArray(size * size).apply {
|
||||
for (y in 0 until size) {
|
||||
for (x in 0 until size) {
|
||||
this[y * size + x] = if (bitMatrix.get(x, y)) colorCode else backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
|
||||
setPixels(pixels, 0, size, 0, 0, size, size)
|
||||
}
|
||||
|
||||
} catch (e: WriterException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成带Logo的二维码
|
||||
* @param content 二维码内容
|
||||
* @param size 二维码边长(像素)
|
||||
* @param logo Logo Bitmap
|
||||
* @return 带Logo的二维码Bitmap
|
||||
*/
|
||||
fun generateQRCodeWithLogo(content: String, size: Int, logo: Bitmap?): Bitmap? {
|
||||
val qrCode = generateQRCode(content, size) ?: return null
|
||||
logo ?: return qrCode
|
||||
|
||||
val logoSize = size / 5 // Logo大小约为二维码的1/5
|
||||
val scaledLogo = Bitmap.createScaledBitmap(logo, logoSize, logoSize, false)
|
||||
|
||||
val offset = (size - logoSize) / 2
|
||||
return Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
|
||||
val canvas = android.graphics.Canvas(this)
|
||||
canvas.drawBitmap(qrCode, 0f, 0f, null)
|
||||
canvas.drawBitmap(scaledLogo, offset.toFloat(), offset.toFloat(), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.sw.inbound.utils
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
@@ -9,6 +9,9 @@ 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 {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.sw.dualscreen.utils
|
||||
|
||||
import android.Manifest
|
||||
import com.sw.dualscreen.activity.BaseActivity
|
||||
|
||||
object StorageUtils {
|
||||
|
||||
fun requestPermission(activity: BaseActivity<*>, callback: () -> Unit) {
|
||||
activity.requestSinglePermissionResult(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
||||
callback()
|
||||
}
|
||||
// activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
|
||||
//// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
//// if (Environment.isExternalStorageManager()) {
|
||||
//// callback()
|
||||
//// } else {
|
||||
//// val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||
////// activity.startActivityForResult(intent, MANAGE_REQUEST_CODE)
|
||||
//// activity.launchActivityResult(intent) {
|
||||
//// callback()
|
||||
//// }
|
||||
//// }
|
||||
//// } else {
|
||||
//
|
||||
//// }
|
||||
// }
|
||||
}
|
||||
|
||||
fun requestCameraPermission(activity: BaseActivity<*>, callback: () -> Unit) {
|
||||
activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sw.dualscreen.view
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
@@ -18,10 +19,14 @@ import com.sw.dualscreen.databinding.BottomSheetDialogBinding
|
||||
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
|
||||
import com.sw.dualscreen.model.response.FoodInfo
|
||||
import com.sw.dualscreen.utils.Debouncer
|
||||
import com.sw.dualscreen.utils.KeyboardUtils
|
||||
import com.sw.dualscreen.viewmodel.UserViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* 底部搜索结果弹窗
|
||||
*/
|
||||
class CustomBottomSheetDialog(
|
||||
val viewModel: UserViewModel,
|
||||
val itemClickCallback: (FoodInfo) -> Unit
|
||||
@@ -29,6 +34,7 @@ class CustomBottomSheetDialog(
|
||||
private lateinit var binding: BottomSheetDialogBinding
|
||||
private lateinit var adapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
|
||||
private var checkedItem: FoodInfo? = null
|
||||
private val debouncer = Debouncer(2000)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -41,13 +47,8 @@ class CustomBottomSheetDialog(
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val debouncer = Debouncer(3000)
|
||||
|
||||
initView()
|
||||
binding.ivSearch.setOnClickListener {
|
||||
debouncer.debounce {
|
||||
viewModel.searchByFoodName(binding.etSearch.text.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -82,6 +83,7 @@ class CustomBottomSheetDialog(
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.viewClose.setOnClickListener { dismiss() }
|
||||
adapter = createAdapter()
|
||||
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
|
||||
// 添加间距装饰(12dp)
|
||||
@@ -93,9 +95,21 @@ class CustomBottomSheetDialog(
|
||||
)
|
||||
)
|
||||
binding.recyclerview.adapter = adapter
|
||||
binding.ivSearch.setOnClickListener {
|
||||
searchInfo()
|
||||
}
|
||||
KeyboardUtils.setupEditorAction(binding.etSearch) {
|
||||
searchInfo()
|
||||
}
|
||||
registerDataChange()
|
||||
}
|
||||
|
||||
private fun searchInfo(){
|
||||
debouncer.debounce {
|
||||
viewModel.searchByFoodName(binding.etSearch.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerDataChange() {
|
||||
lifecycleScope.launch {
|
||||
viewModel.searchFoodInfoList.collect {
|
||||
@@ -131,7 +145,14 @@ class CustomBottomSheetDialog(
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
Timber.d("onCancel")
|
||||
viewModel.cleanSearchFoodInfoList()
|
||||
super.onCancel(dialog)
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
Timber.d("dismiss")
|
||||
viewModel.cleanSearchFoodInfoList()
|
||||
super.dismiss()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.sw.dualscreen.view;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
/**
|
||||
* 加载进度条弹窗
|
||||
*/
|
||||
public class CustomDialog extends Dialog {
|
||||
|
||||
|
||||
/**
|
||||
* 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
|
||||
*/
|
||||
public CustomDialog(Context context, View layout, int style) {
|
||||
|
||||
super(context, style);
|
||||
|
||||
setContentView(layout);
|
||||
|
||||
Window window = getWindow();
|
||||
|
||||
WindowManager.LayoutParams params = window.getAttributes();
|
||||
|
||||
params.gravity = Gravity.CENTER;
|
||||
|
||||
window.setAttributes(params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 宽高由该方法的参数设置
|
||||
*/
|
||||
public CustomDialog(Context context, int width, int height, View layout,
|
||||
int style) {
|
||||
super(context, style);
|
||||
// 设置内容
|
||||
setContentView(layout);
|
||||
// 设置窗口属性
|
||||
Window window = getWindow();
|
||||
WindowManager.LayoutParams params = window.getAttributes();
|
||||
// 设置宽度、高度、密度、对齐方式
|
||||
float density = getDensity(context);
|
||||
params.width = (int) (width * density);
|
||||
params.height = (int) (height * density);
|
||||
params.gravity = Gravity.CENTER;
|
||||
window.setAttributes(params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取显示密度
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public float getDensity(Context context) {
|
||||
Resources res = context.getResources();
|
||||
DisplayMetrics dm = res.getDisplayMetrics();
|
||||
return dm.density;
|
||||
}
|
||||
|
||||
private void fullScreenImmersive(View view) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
view.setSystemUiVisibility(uiOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public void show() {
|
||||
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
super.show();
|
||||
// fullScreenImmersive(getWindow().getDecorView());
|
||||
// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,18 @@ import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
|
||||
import com.sw.dualscreen.R;
|
||||
import com.sw.plate.utils.AppUtil;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* 自定义柱状图
|
||||
*/
|
||||
public class CustomImageView extends androidx.appcompat.widget.AppCompatImageView {
|
||||
private AlphaAnimation alphaAnimation;
|
||||
private String textToDraw;
|
||||
@@ -95,11 +101,21 @@ public class CustomImageView extends androidx.appcompat.widget.AppCompatImageVie
|
||||
}
|
||||
|
||||
public void setCustomHeightPercent(float percent, boolean isAnimation) {
|
||||
setCustomHeight((int) (percent * MAX_HEIGHT), isAnimation);
|
||||
Timber.d("setCustomHeightPercent percent = %s", percent);
|
||||
if (percent<= 0){
|
||||
percent = 0f;
|
||||
}
|
||||
setCustomHeight((int) (percent * MAX_HEIGHT), false);
|
||||
}
|
||||
|
||||
// 用于设置自定义高度的方法
|
||||
public void setCustomHeight(int height, boolean isAnimation) {
|
||||
Timber.d("setCustomHeight height = %s", height);
|
||||
|
||||
setVisibility(height == 0 ? View.GONE : VISIBLE);
|
||||
if (height > MAX_HEIGHT){
|
||||
height = MAX_HEIGHT;
|
||||
}
|
||||
ViewGroup.LayoutParams layoutParams = getLayoutParams();
|
||||
int height1 = layoutParams.height;
|
||||
if (height1 != height) {
|
||||
|
||||
@@ -15,6 +15,9 @@ import androidx.appcompat.widget.AppCompatSpinner
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.sw.dualscreen.R
|
||||
|
||||
/**
|
||||
* spinner控件
|
||||
*/
|
||||
class CustomSpinner @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,10 @@ abstract class BaseViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
|
||||
if (response.isSuccess()) {
|
||||
if (response.isSuccess()
|
||||
|| response.code == 200
|
||||
|| response.code == 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
Timber.d("msg = ${response.message}, code = ${response.code}")
|
||||
@@ -62,13 +65,13 @@ abstract class BaseViewModel() : ViewModel() {
|
||||
|
||||
fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
|
||||
GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
|
||||
GlobalData.appBaseUrl="http://192.168.1.210/gateway/local"
|
||||
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}")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.sw.dualscreen.viewmodel
|
||||
|
||||
import com.sw.dualscreen.GlobalData
|
||||
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 com.sw.dualscreen.utils.SPUtil
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import timber.log.Timber
|
||||
@@ -18,7 +19,7 @@ class DeviceViewModel : BaseViewModel() {
|
||||
/**
|
||||
* 获取token
|
||||
*/
|
||||
fun getDeviceToken(deviceId: String = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e") {
|
||||
fun getDeviceToken(deviceId: String = GlobalData.deviceId) {
|
||||
launchWithLoading {
|
||||
val response = repository.getDeviceToken(deviceId)
|
||||
if (parseResponse(response)) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.sw.dualscreen.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.arcsoft.face.ErrorInfo
|
||||
import com.sw.dualscreen.GlobalData
|
||||
@@ -9,19 +11,19 @@ import com.sw.dualscreen.model.response.DinnerTypeInfo
|
||||
import com.sw.dualscreen.model.response.FoodInfo
|
||||
import com.sw.dualscreen.model.response.UserFaceModel
|
||||
import com.sw.dualscreen.model.response.UserNutritionData
|
||||
import com.sw.inbound.utils.SPUtil
|
||||
import com.sw.dualscreen.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
|
||||
import kotlin.math.atan
|
||||
|
||||
class UserViewModel : BaseViewModel() {
|
||||
|
||||
@@ -40,16 +42,23 @@ class UserViewModel : BaseViewModel() {
|
||||
val nutritionData: StateFlow<UserNutritionData?> = _nutritionData
|
||||
|
||||
/**
|
||||
* 饭点类型
|
||||
* 饭点类型 早餐/午餐/晚餐
|
||||
*/
|
||||
private val _dinnerTypeInfo = MutableStateFlow<DinnerTypeInfo?>(null)
|
||||
val dinnerTypeInfo: StateFlow<DinnerTypeInfo?> = _dinnerTypeInfo
|
||||
|
||||
/**
|
||||
* 人脸加载完成
|
||||
*/
|
||||
private val _loadFaceResult = MutableStateFlow<Boolean>(false)
|
||||
val loadFaceResult: StateFlow<Boolean> = _loadFaceResult
|
||||
private val _identifiedFoodInfoList2 = MutableStateFlow<List<FoodInfo>>(emptyList())
|
||||
val identifiedFoodInfoList2: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList2
|
||||
|
||||
/**
|
||||
* 获取token
|
||||
*/
|
||||
fun getEquipmentToken(qrcodeId: String = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e") {
|
||||
fun getEquipmentToken(qrcodeId: String = GlobalData.deviceId) {
|
||||
Timber.d("getEquipmentToken")
|
||||
launchWithLoading {
|
||||
val response = repository.getEquipmentToken(qrcodeId)
|
||||
@@ -70,6 +79,7 @@ class UserViewModel : BaseViewModel() {
|
||||
fun getUserFaceCache(index: Int = 0) {
|
||||
Timber.d("getUserFaceCache index = $index")
|
||||
launch {
|
||||
_loadFaceResult.value = false
|
||||
val response = repository.getUserFaceCache(index)
|
||||
if (parseResponse(response)) {
|
||||
// 获取成功一次后缓存状态
|
||||
@@ -80,11 +90,12 @@ class UserViewModel : BaseViewModel() {
|
||||
FaceEntity(it.userId, null, Base64.decode(it.faceFeatureString))
|
||||
}
|
||||
faceApi.updateFaceData(index, faceEntity)
|
||||
activeEngine()
|
||||
}
|
||||
val nextPageIndex = response.result?.nextPageIndex ?: -1
|
||||
if (nextPageIndex > 0) {
|
||||
getUserFaceCache(nextPageIndex)
|
||||
} else {
|
||||
_loadFaceResult.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,25 +140,13 @@ class UserViewModel : BaseViewModel() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户信息
|
||||
*/
|
||||
fun resetUserInfo() {
|
||||
Timber.d("resetUserInfo")
|
||||
}
|
||||
|
||||
fun getIdentifiedFoodList() {
|
||||
Timber.d("identifiedFoodList")
|
||||
launchWithLoading {
|
||||
delay(5000)
|
||||
_identifiedFoodInfoList.value = List(6) {
|
||||
FoodInfo(
|
||||
id = "185225109358109491${it}",
|
||||
foodName = "豆芽炒粉条$it",
|
||||
imgUrl = "http://101.201.149.156:9002/temp/20230912/f54b2d76-45e1-4c6b-a7a0-5fd1eace99d1_1694484601879.png"
|
||||
)
|
||||
val response = repository.getRestInfoFoodsByType(foodName = "")
|
||||
if (parseResponse(response)) {
|
||||
_identifiedFoodInfoList.value = response.result ?: emptyList()
|
||||
}
|
||||
// _currentFoodInfo.value = _identifiedFoodInfoList.value[0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +178,8 @@ class UserViewModel : BaseViewModel() {
|
||||
*/
|
||||
fun getUserNutritionData(userId: String, foodId: String) {
|
||||
Timber.d("getUserNutritionData userId = ${userId}, foodId = $foodId")
|
||||
launchWithLoading {
|
||||
_nutritionData.value = null
|
||||
launch {
|
||||
val response = repository.getUserNutritionData(userId = userId, foodId = foodId)
|
||||
if (parseResponse(response)) {
|
||||
_nutritionData.value = response.result
|
||||
@@ -189,7 +189,8 @@ class UserViewModel : BaseViewModel() {
|
||||
|
||||
fun getDinnerType() {
|
||||
Timber.d("getDinnerType")
|
||||
launchWithLoading {
|
||||
_dinnerTypeInfo.value = null
|
||||
launch {
|
||||
val response = repository.getDinnerType()
|
||||
if (parseResponse(response)) {
|
||||
_dinnerTypeInfo.value = response.result
|
||||
@@ -200,13 +201,44 @@ class UserViewModel : BaseViewModel() {
|
||||
/**
|
||||
* 提交就餐数据
|
||||
*/
|
||||
fun postUserNutritionData(param: UserNutritionParam) {
|
||||
fun postUserNutritionData(param: List<UserNutritionParam>) {
|
||||
Timber.d("postUserNutritionData param = $param")
|
||||
launchWithLoading {
|
||||
launch {
|
||||
val response = repository.postUserNutritionData(param)
|
||||
if (parseResponse(response)) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFoodInfo(foodName: String, action:(List<FoodInfo>)-> Unit = {}) {
|
||||
Timber.d("getFoodInfo")
|
||||
launchWithLoading {
|
||||
val response = repository.getFoodInfo(foodName = foodName)
|
||||
if (parseResponse(response)) {
|
||||
val list = response.result ?: emptyList()
|
||||
_identifiedFoodInfoList2.value = list
|
||||
action(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun postImageData(
|
||||
context: Context,
|
||||
foodId: String,
|
||||
foodName: String,
|
||||
foodVector: String,
|
||||
uri: Uri
|
||||
) {
|
||||
Timber.d("postImageData")
|
||||
launchWithLoading {
|
||||
val response = repository.postImageData(
|
||||
context, foodId = foodId, foodName = foodName, foodVector = foodVector,
|
||||
uri = uri
|
||||
)
|
||||
if (parseResponse(response)) {
|
||||
Timber.tag("mzf").d("code=${response.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#FF3232" android:state_checked="true"/>
|
||||
<item android:color="#5E7585" android:state_checked="false"/>
|
||||
</selector>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#30000000">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FF0032C8" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 172 KiB |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#ffffff"/>
|
||||
<stroke android:color="#cdcdcd" android:width="1dp"/>
|
||||
<corners android:radius="5dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#ff08C6DC" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:state_checked="false">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="8dp"/>
|
||||
<solid android:color="#F4FAFF"/>
|
||||
<stroke android:color="@color/white" android:width="2dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_checked="true">
|
||||
<layer-list>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="8dp"/>
|
||||
<solid android:color="#FFF0F0"/>
|
||||
<stroke android:color="#FF3232" android:width="2dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:width="32dp" android:height="32dp" android:gravity="bottom|end">
|
||||
<bitmap android:src="@drawable/check_red"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
|
||||
</selector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<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:color="#ffff9900"
|
||||
android:width="2dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/white"/>
|
||||
<corners android:radius="12dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#30000000">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/white"/>
|
||||
<stroke android:color="#FF0032C8" android:width="1dp"/>
|
||||
<corners android:radius="10dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -2,6 +2,6 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@android:color/white" />
|
||||
<corners
|
||||
android:topLeftRadius="16dp"
|
||||
android:topRightRadius="16dp" />
|
||||
android:topLeftRadius="60dp"
|
||||
android:topRightRadius="60dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="5dp" />
|
||||
<solid android:color="@color/black" />
|
||||
</shape>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="false" >
|
||||
<layer-list>
|
||||
<item android:width="60dp" android:height="60dp">
|
||||
<bitmap android:src="@drawable/pay_cash_normal" />
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
<item android:state_checked="true">
|
||||
<layer-list>
|
||||
<item android:width="60dp" android:height="60dp">
|
||||
<bitmap android:src="@drawable/pay_cash_checked" />
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="false" >
|
||||
<layer-list>
|
||||
<item android:width="60dp" android:height="60dp">
|
||||
<bitmap android:src="@drawable/pay_face_normal" />
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
<item android:state_checked="true">
|
||||
<layer-list>
|
||||
<item android:width="60dp" android:height="60dp">
|
||||
<bitmap android:src="@drawable/pay_face_checked" />
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="false" >
|
||||
<layer-list>
|
||||
<item android:width="60dp" android:height="60dp">
|
||||
<bitmap android:src="@drawable/pay_number_normal" />
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
<item android:state_checked="true">
|
||||
<layer-list>
|
||||
<item android:width="60dp" android:height="60dp">
|
||||
<bitmap android:src="@drawable/pay_number_checked" />
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
</selector>
|
||||