迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)
@@ -9,7 +9,16 @@ captures/
|
||||
.DS_Store
|
||||
training/datasets/
|
||||
training/venv/
|
||||
training/runs/
|
||||
*.pt
|
||||
*.tflite
|
||||
|
||||
# 后端运行时数据(SQLite 库,删除即丢授权/订单)
|
||||
server/data/
|
||||
server/biz/service/testdata/*.db
|
||||
|
||||
# 管理端构建产物(server_admin 构建生成,由后端托管)
|
||||
server/admin_dist/
|
||||
|
||||
# 文生图配置(含API key,不入库)
|
||||
#training/gen_images_config.json
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.observer"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.observer"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
implementation(libs.androidx.camera.core)
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.lifecycle)
|
||||
implementation(libs.androidx.camera.view)
|
||||
|
||||
implementation(libs.org.tensorflow.lite)
|
||||
implementation(libs.org.tensorflow.lite.gpu)
|
||||
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# TensorFlow Lite
|
||||
-keep class org.tensorflow.** { *; }
|
||||
-dontwarn org.tensorflow.**
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-feature android:name="android.hardware.camera.any" android:required="true" />
|
||||
|
||||
<application
|
||||
android:name=".ObserverApp"
|
||||
android:allowBackup="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Observer">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.example.observer
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import com.example.observer.ui.ObserverApp
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
ObserverApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.example.observer
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.example.observer.data.SettingsRepository
|
||||
import com.example.observer.detection.Detector
|
||||
import com.example.observer.detection.TFLiteDetector
|
||||
import com.example.observer.distance.DistanceEstimator
|
||||
import com.example.observer.reminder.Reminder
|
||||
|
||||
class ObserverApp : Application() {
|
||||
lateinit var container: AppContainer
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
container = AppContainer(this)
|
||||
}
|
||||
}
|
||||
|
||||
class AppContainer(context: Context) {
|
||||
val settingsRepository = SettingsRepository(context)
|
||||
val detector: Detector? = TFLiteDetector.create(context)
|
||||
val distanceEstimator = DistanceEstimator(context)
|
||||
val reminder = Reminder(context)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.example.observer.camera
|
||||
|
||||
import android.util.Log
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.camera2.interop.Camera2CameraInfo
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class CameraController(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val analysisExecutor: Executor,
|
||||
) {
|
||||
var onCameraIdChanged: ((String) -> Unit)? = null
|
||||
|
||||
var onInitError: (() -> Unit)? = null
|
||||
|
||||
var currentCameraId: String? = null
|
||||
private set
|
||||
|
||||
private var cameraProvider: ProcessCameraProvider? = null
|
||||
private var isFrontFacing = false
|
||||
|
||||
fun start(previewView: PreviewView, analyzer: ImageAnalysis.Analyzer?) {
|
||||
val future = ProcessCameraProvider.getInstance(previewView.context)
|
||||
future.addListener({
|
||||
try {
|
||||
val provider = future.get()
|
||||
cameraProvider = provider
|
||||
bind(previewView, analyzer)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "相机初始化失败", e)
|
||||
onInitError?.invoke()
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(previewView.context))
|
||||
}
|
||||
|
||||
fun switchCamera(previewView: PreviewView, analyzer: ImageAnalysis.Analyzer?) {
|
||||
isFrontFacing = !isFrontFacing
|
||||
bind(previewView, analyzer)
|
||||
}
|
||||
|
||||
private fun bind(previewView: PreviewView, analyzer: ImageAnalysis.Analyzer?) {
|
||||
val provider = cameraProvider ?: return
|
||||
provider.unbindAll()
|
||||
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.surfaceProvider = previewView.surfaceProvider
|
||||
}
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
|
||||
.build()
|
||||
analyzer?.let { imageAnalysis.setAnalyzer(analysisExecutor, it) }
|
||||
|
||||
val camera: Camera = provider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
currentSelector(),
|
||||
preview,
|
||||
imageAnalysis,
|
||||
)
|
||||
currentCameraId = try {
|
||||
Camera2CameraInfo.from(camera.cameraInfo).cameraId
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "获取 cameraId 失败", e)
|
||||
null
|
||||
}
|
||||
currentCameraId?.let { onCameraIdChanged?.invoke(it) }
|
||||
}
|
||||
|
||||
private fun currentSelector(): CameraSelector =
|
||||
if (isFrontFacing) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CameraController"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.example.observer.camera
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import com.example.observer.detection.DetectionResult
|
||||
import com.example.observer.detection.Detector
|
||||
import com.example.observer.detection.MotionRegion
|
||||
|
||||
class FrameAnalyzer(
|
||||
private val detector: Detector,
|
||||
private val motionDetector: MotionDetector,
|
||||
private val onResult: (results: List<DetectionResult>, rotation: Int, imageWidthPx: Int, imageHeightPx: Int, motionRegions: List<MotionRegion>) -> Unit,
|
||||
) : ImageAnalysis.Analyzer {
|
||||
|
||||
@Volatile var intervalMs: Long = 100L
|
||||
|
||||
private var lastDetectMs = 0L
|
||||
|
||||
override fun analyze(imageProxy: ImageProxy) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (now - lastDetectMs < intervalMs) {
|
||||
imageProxy.close()
|
||||
return
|
||||
}
|
||||
lastDetectMs = now
|
||||
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
val rotation = imageProxy.imageInfo.rotationDegrees
|
||||
try {
|
||||
val motionRegions = motionDetector.detectMotion(bitmap)
|
||||
val results = detector.detect(bitmap)
|
||||
onResult(results, rotation, bitmap.width, bitmap.height, motionRegions)
|
||||
} finally {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.example.observer.camera
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import com.example.observer.detection.MotionAggregator
|
||||
import com.example.observer.detection.MotionRegion
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* 轻量运动检测:相邻帧灰度差分 + 分块聚合。
|
||||
* 小尺寸工作(约 128x128 内),每帧开销亚毫秒级,在分析线程串行调用。
|
||||
* 相机大幅移动时(全屏帧差)自动忽略本帧,避免误报。
|
||||
*/
|
||||
class MotionDetector(
|
||||
private val maxWidth: Int = 128,
|
||||
private val maxHeight: Int = 128,
|
||||
) {
|
||||
|
||||
private val smallBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Bitmap.Config.ARGB_8888)
|
||||
private val canvas = Canvas(smallBitmap)
|
||||
private val paint = Paint(Paint.FILTER_BITMAP_FLAG)
|
||||
|
||||
private var prevGray: IntArray? = null
|
||||
private var grayCache: IntArray = IntArray(0)
|
||||
|
||||
fun detectMotion(bitmap: Bitmap): List<MotionRegion> {
|
||||
val scale = min(maxWidth.toFloat() / bitmap.width, maxHeight.toFloat() / bitmap.height)
|
||||
val w = (bitmap.width * scale).toInt()
|
||||
val h = (bitmap.height * scale).toInt()
|
||||
if (w == 0 || h == 0) return emptyList()
|
||||
|
||||
canvas.drawBitmap(bitmap, null, Rect(0, 0, w, h), paint)
|
||||
if (grayCache.size != w * h) grayCache = IntArray(w * h)
|
||||
smallBitmap.getPixels(grayCache, 0, w, 0, 0, w, h)
|
||||
|
||||
val prev = prevGray
|
||||
prevGray = grayCache.copyOf()
|
||||
if (prev == null || prev.size != grayCache.size) return emptyList()
|
||||
|
||||
// 取绿色通道近似亮度
|
||||
val gray = IntArray(grayCache.size)
|
||||
for (i in grayCache.indices) gray[i] = (grayCache[i] shr 8) and 0xFF
|
||||
val prevGreen = IntArray(prev.size)
|
||||
for (i in prev.indices) prevGreen[i] = (prev[i] shr 8) and 0xFF
|
||||
|
||||
val diff = MotionAggregator.diffMask(gray, prevGreen)
|
||||
val motionTotal = diff.sum()
|
||||
// 全屏大差异 → 相机移动/大范围变化,忽略本帧
|
||||
if (motionTotal > w * h / 2) return emptyList()
|
||||
if (motionTotal < 12) return emptyList()
|
||||
return MotionAggregator.aggregate(diff, w, h)
|
||||
}
|
||||
|
||||
/** 相机切换后重置参考帧,避免旧帧误差 */
|
||||
fun reset() {
|
||||
prevGray = null
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package com.example.observer.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
enum class DetectMode(val intervalMs: Long, val label: String) {
|
||||
CONTINUOUS(100L, "连续"),
|
||||
STANDARD(300L, "标准"),
|
||||
POWER_SAVING(1000L, "省电"),
|
||||
}
|
||||
|
||||
data class AppSettings(
|
||||
val confThreshold: Float = 0.40f,
|
||||
val habitatThreshold: Float = 0.35f,
|
||||
val detectMode: DetectMode = DetectMode.CONTINUOUS,
|
||||
val vibrateEnabled: Boolean = true,
|
||||
val soundEnabled: Boolean = true,
|
||||
val showDistance: Boolean = true,
|
||||
val habitatAlertEnabled: Boolean = true,
|
||||
)
|
||||
|
||||
private val Context.settingsDataStore by preferencesDataStore(name = "settings")
|
||||
|
||||
class SettingsRepository(private val context: Context) {
|
||||
|
||||
val settings: Flow<AppSettings> = context.settingsDataStore.data.map { prefs ->
|
||||
AppSettings(
|
||||
confThreshold = prefs[KEY_CONF] ?: 0.40f,
|
||||
habitatThreshold = prefs[KEY_HABITAT] ?: 0.35f,
|
||||
detectMode = DetectMode.entries.firstOrNull { it.name == prefs[KEY_MODE] }
|
||||
?: DetectMode.CONTINUOUS,
|
||||
vibrateEnabled = prefs[KEY_VIBRATE] ?: true,
|
||||
soundEnabled = prefs[KEY_SOUND] ?: true,
|
||||
showDistance = prefs[KEY_DISTANCE] ?: true,
|
||||
habitatAlertEnabled = prefs[KEY_HABITAT_ALERT] ?: true,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setConfThreshold(value: Float) =
|
||||
context.settingsDataStore.edit { it[KEY_CONF] = value }
|
||||
|
||||
suspend fun setHabitatThreshold(value: Float) =
|
||||
context.settingsDataStore.edit { it[KEY_HABITAT] = value }
|
||||
|
||||
suspend fun setDetectMode(mode: DetectMode) =
|
||||
context.settingsDataStore.edit { it[KEY_MODE] = mode.name }
|
||||
|
||||
suspend fun setVibrateEnabled(value: Boolean) =
|
||||
context.settingsDataStore.edit { it[KEY_VIBRATE] = value }
|
||||
|
||||
suspend fun setSoundEnabled(value: Boolean) =
|
||||
context.settingsDataStore.edit { it[KEY_SOUND] = value }
|
||||
|
||||
suspend fun setShowDistance(value: Boolean) =
|
||||
context.settingsDataStore.edit { it[KEY_DISTANCE] = value }
|
||||
|
||||
suspend fun setHabitatAlertEnabled(value: Boolean) =
|
||||
context.settingsDataStore.edit { it[KEY_HABITAT_ALERT] = value }
|
||||
|
||||
companion object {
|
||||
private val KEY_CONF = floatPreferencesKey("conf_threshold")
|
||||
private val KEY_HABITAT = floatPreferencesKey("habitat_threshold")
|
||||
private val KEY_MODE = stringPreferencesKey("detect_mode")
|
||||
private val KEY_VIBRATE = booleanPreferencesKey("vibrate_enabled")
|
||||
private val KEY_SOUND = booleanPreferencesKey("sound_enabled")
|
||||
private val KEY_DISTANCE = booleanPreferencesKey("show_distance")
|
||||
private val KEY_HABITAT_ALERT = booleanPreferencesKey("habitat_alert_enabled")
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
/** 预览视图像素坐标矩形(纯 JVM 类型,便于单元测试) */
|
||||
data class ViewRect(
|
||||
val left: Float,
|
||||
val top: Float,
|
||||
val right: Float,
|
||||
val bottom: Float,
|
||||
) {
|
||||
val width: Float get() = right - left
|
||||
val height: Float get() = bottom - top
|
||||
val centerX: Float get() = (left + right) / 2f
|
||||
val centerY: Float get() = (top + bottom) / 2f
|
||||
}
|
||||
|
||||
object CoordinateMapper {
|
||||
|
||||
/** 模型归一化坐标 → 预览视图坐标(含传感器旋转与 FIT_CENTER 裁剪) */
|
||||
fun mapToView(
|
||||
normLeft: Float,
|
||||
normTop: Float,
|
||||
normRight: Float,
|
||||
normBottom: Float,
|
||||
rotation: Int, // ImageProxy.imageInfo.rotationDegrees
|
||||
imageW: Int,
|
||||
imageH: Int, // 分析图像尺寸(横屏原图)
|
||||
viewW: Int,
|
||||
viewH: Int, // 预览视图尺寸
|
||||
): ViewRect {
|
||||
// 1) 旋转校正:图像方向 → 竖屏视图方向(归一化坐标)
|
||||
val (x0, y0, x1, y1) = when (rotation) {
|
||||
90 -> Quad(1 - normBottom, normLeft, 1 - normTop, normRight)
|
||||
180 -> Quad(1 - normRight, 1 - normBottom, 1 - normLeft, 1 - normTop)
|
||||
270 -> Quad(normTop, 1 - normRight, normBottom, 1 - normLeft)
|
||||
else -> Quad(normLeft, normTop, normRight, normBottom)
|
||||
}
|
||||
// 2) 旋转后图像在竖屏方向上的尺寸
|
||||
val portrait = rotation == 90 || rotation == 270
|
||||
val portW = if (portrait) imageH else imageW
|
||||
val portH = if (portrait) imageW else imageH
|
||||
// 3) FIT_CENTER 缩放与居中偏移
|
||||
val scale = minOf(viewW.toFloat() / portW, viewH.toFloat() / portH)
|
||||
val offsetX = (viewW - portW * scale) / 2f
|
||||
val offsetY = (viewH - portH * scale) / 2f
|
||||
return ViewRect(
|
||||
left = x0 * portW * scale + offsetX,
|
||||
top = y0 * portH * scale + offsetY,
|
||||
right = x1 * portW * scale + offsetX,
|
||||
bottom = y1 * portH * scale + offsetY,
|
||||
)
|
||||
}
|
||||
|
||||
private data class Quad(val x0: Float, val y0: Float, val x1: Float, val y1: Float)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
data class DetectionResult(
|
||||
val label: String,
|
||||
val score: Float,
|
||||
val left: Float,
|
||||
val top: Float,
|
||||
val right: Float,
|
||||
val bottom: Float,
|
||||
val distanceM: Float? = null,
|
||||
) {
|
||||
val width: Float get() = right - left
|
||||
val height: Float get() = bottom - top
|
||||
val centerX: Float get() = (left + right) / 2f
|
||||
val centerY: Float get() = (top + bottom) / 2f
|
||||
val isHabitat: Boolean get() = label == HABITAT_LABEL
|
||||
|
||||
companion object {
|
||||
const val HABITAT_LABEL = "cover"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
interface Detector {
|
||||
/** 输入 RGBA 位图,输出归一化坐标检测结果(0..1) */
|
||||
fun detect(bitmap: Bitmap): List<DetectionResult>
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* 帧差运动聚合(纯 JVM,可单测)。
|
||||
* 输入:每像素 0/1 差分掩码,按 8x8 分块统计激活块,连通块聚合为运动区域。
|
||||
*/
|
||||
object MotionAggregator {
|
||||
|
||||
private const val BLOCK_GRID = 8
|
||||
private const val BLOCK_ACTIVE_RATIO = 0.30f
|
||||
private const val MAX_REGIONS = 3
|
||||
|
||||
fun aggregate(
|
||||
diff: IntArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
): List<MotionRegion> {
|
||||
val bw = width / BLOCK_GRID
|
||||
val bh = height / BLOCK_GRID
|
||||
if (bw == 0 || bh == 0) return emptyList()
|
||||
|
||||
val active = BooleanArray(BLOCK_GRID * BLOCK_GRID)
|
||||
for (by in 0 until BLOCK_GRID) {
|
||||
for (bx in 0 until BLOCK_GRID) {
|
||||
val blockW = if (bx == BLOCK_GRID - 1) width - bx * bw else bw
|
||||
val blockH = if (by == BLOCK_GRID - 1) height - by * bh else bh
|
||||
var motion = 0
|
||||
for (y in by * bh until by * bh + blockH) {
|
||||
var idx = y * width + bx * bw
|
||||
for (x in 0 until blockW) {
|
||||
motion += diff[idx + x]
|
||||
}
|
||||
idx += width
|
||||
}
|
||||
active[by * BLOCK_GRID + bx] = motion > blockW * blockH * BLOCK_ACTIVE_RATIO
|
||||
}
|
||||
}
|
||||
|
||||
// 连通块聚合(4 邻域)
|
||||
val regions = ArrayList<MotionRegion>(MAX_REGIONS)
|
||||
val visited = BooleanArray(active.size)
|
||||
for (i in active.indices) {
|
||||
if (!active[i] || visited[i]) continue
|
||||
var minX = BLOCK_GRID
|
||||
var minY = BLOCK_GRID
|
||||
var maxX = -1
|
||||
var maxY = -1
|
||||
val stack = ArrayDeque<Int>()
|
||||
stack.add(i)
|
||||
visited[i] = true
|
||||
while (stack.isNotEmpty()) {
|
||||
val cur = stack.removeLast()
|
||||
val bx = cur % BLOCK_GRID
|
||||
val by = cur / BLOCK_GRID
|
||||
minX = minOf(minX, bx)
|
||||
maxX = maxOf(maxX, bx)
|
||||
minY = minOf(minY, by)
|
||||
maxY = maxOf(maxY, by)
|
||||
for (nb in neighbors(cur)) {
|
||||
if (active[nb] && !visited[nb]) {
|
||||
visited[nb] = true
|
||||
stack.add(nb)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxX - minX > 3 || maxY - minY > 3) continue // 全屏噪声过滤
|
||||
regions += MotionRegion(
|
||||
left = minX * bw / width.toFloat(),
|
||||
top = minY * bh / height.toFloat(),
|
||||
right = ((maxX + 1) * bw).coerceAtMost(width) / width.toFloat(),
|
||||
bottom = ((maxY + 1) * bh).coerceAtMost(height) / height.toFloat(),
|
||||
)
|
||||
if (regions.size >= MAX_REGIONS) break
|
||||
}
|
||||
return regions
|
||||
}
|
||||
|
||||
private fun neighbors(i: Int): List<Int> {
|
||||
val bx = i % BLOCK_GRID
|
||||
val by = i / BLOCK_GRID
|
||||
val list = ArrayList<Int>(4)
|
||||
if (bx > 0) list += i - 1
|
||||
if (bx < BLOCK_GRID - 1) list += i + 1
|
||||
if (by > 0) list += i - BLOCK_GRID
|
||||
if (by < BLOCK_GRID - 1) list += i + BLOCK_GRID
|
||||
return list
|
||||
}
|
||||
|
||||
/** 检测框中心是否落在运动区域内(用于置信度提升判定) */
|
||||
fun centerInRegion(box: DetectionResult, region: MotionRegion): Boolean =
|
||||
box.centerX in region.left..region.right && box.centerY in region.top..region.bottom
|
||||
|
||||
/** 帧差掩码生成(纯 JVM):|g - prev| > threshold → 1 */
|
||||
fun diffMask(gray: IntArray, prev: IntArray, threshold: Int = 25): IntArray {
|
||||
val diff = IntArray(gray.size)
|
||||
for (i in gray.indices) {
|
||||
diff[i] = if (abs(gray[i] - prev[i]) > threshold) 1 else 0
|
||||
}
|
||||
return diff
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
/** 运动区域(归一化坐标) */
|
||||
data class MotionRegion(
|
||||
val left: Float,
|
||||
val top: Float,
|
||||
val right: Float,
|
||||
val bottom: Float,
|
||||
) {
|
||||
val centerX: Float get() = (left + right) / 2f
|
||||
val centerY: Float get() = (top + bottom) / 2f
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
fun iou(a: DetectionResult, b: DetectionResult): Float {
|
||||
val x0 = maxOf(a.left, b.left)
|
||||
val y0 = maxOf(a.top, b.top)
|
||||
val x1 = minOf(a.right, b.right)
|
||||
val y1 = minOf(a.bottom, b.bottom)
|
||||
if (x1 <= x0 || y1 <= y0) return 0f
|
||||
val inter = (x1 - x0) * (y1 - y0)
|
||||
val union = a.width * a.height + b.width * b.height - inter
|
||||
return if (union <= 0f) 0f else inter / union
|
||||
}
|
||||
|
||||
fun nms(boxes: List<DetectionResult>, iouThreshold: Float): List<DetectionResult> {
|
||||
val sorted = boxes.sortedByDescending { it.score }
|
||||
val kept = mutableListOf<DetectionResult>()
|
||||
for (b in sorted) {
|
||||
if (kept.none { iou(b, it) > iouThreshold }) kept += b
|
||||
}
|
||||
return kept
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import org.tensorflow.lite.Interpreter
|
||||
import org.tensorflow.lite.gpu.GpuDelegate
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* YOLOv8n 端侧推理实现。
|
||||
* 模型输出布局(ultralytics tflite 导出):[1, 4 + nc, 8400],
|
||||
* cx/cy/w/h 已归一化,类别得分已过 sigmoid;按列平铺:index = c * 8400 + anchor。
|
||||
*/
|
||||
class TFLiteDetector private constructor(
|
||||
private val interpreter: Interpreter,
|
||||
private val labels: List<String>,
|
||||
) : Detector {
|
||||
|
||||
@Volatile var confThreshold: Float = 0.40f
|
||||
|
||||
@Volatile var habitatThreshold: Float = 0.35f
|
||||
|
||||
private val scaledBitmap = Bitmap.createBitmap(INPUT_SIZE, INPUT_SIZE, Bitmap.Config.ARGB_8888)
|
||||
private val scaleCanvas = Canvas(scaledBitmap)
|
||||
private val scaleMatrix = Matrix()
|
||||
private val pixels = IntArray(INPUT_SIZE * INPUT_SIZE)
|
||||
|
||||
private val inputBuffer: ByteBuffer =
|
||||
ByteBuffer.allocateDirect(1 * INPUT_SIZE * INPUT_SIZE * 3 * 4)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
|
||||
private val outputFloats = FloatArray((4 + labels.size) * NUM_ANCHORS)
|
||||
|
||||
override fun detect(bitmap: Bitmap): List<DetectionResult> {
|
||||
preprocess(bitmap)
|
||||
interpreter.run(inputBuffer, outputFloats)
|
||||
return postprocess()
|
||||
}
|
||||
|
||||
private fun preprocess(bitmap: Bitmap) {
|
||||
// CENTER_CROP 缩放保持宽高比
|
||||
val scale = maxOf(INPUT_SIZE.toFloat() / bitmap.width, INPUT_SIZE.toFloat() / bitmap.height)
|
||||
val dx = (INPUT_SIZE - bitmap.width * scale) / 2f
|
||||
val dy = (INPUT_SIZE - bitmap.height * scale) / 2f
|
||||
scaleMatrix.reset()
|
||||
scaleMatrix.setScale(scale, scale)
|
||||
scaleMatrix.postTranslate(dx, dy)
|
||||
scaleCanvas.drawBitmap(bitmap, scaleMatrix, null)
|
||||
|
||||
inputBuffer.rewind()
|
||||
scaledBitmap.getPixels(pixels, 0, INPUT_SIZE, 0, 0, INPUT_SIZE, INPUT_SIZE)
|
||||
for (p in pixels) {
|
||||
inputBuffer.putFloat(((p shr 16) and 0xFF) / 255f)
|
||||
inputBuffer.putFloat(((p shr 8) and 0xFF) / 255f)
|
||||
inputBuffer.putFloat((p and 0xFF) / 255f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postprocess(): List<DetectionResult> {
|
||||
val boxes = ArrayList<DetectionResult>(8)
|
||||
for (a in 0 until NUM_ANCHORS) {
|
||||
val cx = outputFloats[a]
|
||||
val cy = outputFloats[NUM_ANCHORS + a]
|
||||
val w = outputFloats[2 * NUM_ANCHORS + a]
|
||||
val h = outputFloats[3 * NUM_ANCHORS + a]
|
||||
var bestCls = 0
|
||||
var bestScore = 0f
|
||||
for (c in 0 until labels.size) {
|
||||
val s = outputFloats[(4 + c) * NUM_ANCHORS + a]
|
||||
if (s > bestScore) {
|
||||
bestScore = s
|
||||
bestCls = c
|
||||
}
|
||||
}
|
||||
val label = labels.getOrElse(bestCls) { "unknown" }
|
||||
// cover 用独立阈值;动物类保留低分池,供运动检测提升
|
||||
val threshold = if (label == DetectionResult.HABITAT_LABEL) habitatThreshold else MIN_SCORE
|
||||
if (bestScore < threshold) continue
|
||||
boxes += DetectionResult(
|
||||
label = label,
|
||||
score = bestScore,
|
||||
left = (cx - w / 2f).coerceIn(0f, 1f),
|
||||
top = (cy - h / 2f).coerceIn(0f, 1f),
|
||||
right = (cx + w / 2f).coerceIn(0f, 1f),
|
||||
bottom = (cy + h / 2f).coerceIn(0f, 1f),
|
||||
)
|
||||
}
|
||||
return nms(boxes, IOU_THRESHOLD).take(MAX_DETECTIONS)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INPUT_SIZE = 320
|
||||
|
||||
/** 动物类最低保留分数:低于此分不输出(低分候选由运动检测提升显示) */
|
||||
const val MIN_SCORE = 0.20f
|
||||
private const val NUM_ANCHORS = 8400
|
||||
private const val IOU_THRESHOLD = 0.45f
|
||||
private const val MAX_DETECTIONS = 20
|
||||
private const val MODEL_ASSET = "model.tflite"
|
||||
private const val LABELS_ASSET = "labels.txt"
|
||||
|
||||
/** 模型缺失或加载失败返回 null(App 降级为仅预览) */
|
||||
fun create(context: Context): TFLiteDetector? {
|
||||
return try {
|
||||
val options = Interpreter.Options().apply {
|
||||
setNumThreads(4)
|
||||
try {
|
||||
addDelegate(GpuDelegate())
|
||||
} catch (_: Throwable) {
|
||||
// GPU 不可用,回退 CPU
|
||||
}
|
||||
}
|
||||
val interpreter = Interpreter(loadModelFile(context), options)
|
||||
val labels = context.assets.open(LABELS_ASSET).bufferedReader().readLines()
|
||||
TFLiteDetector(interpreter, labels)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadModelFile(context: Context): ByteBuffer {
|
||||
val bytes = context.assets.open(MODEL_ASSET).use { it.readBytes() }
|
||||
return ByteBuffer.allocateDirect(bytes.size).order(ByteOrder.nativeOrder())
|
||||
.apply { put(bytes); rewind() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.example.observer.distance
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.tan
|
||||
|
||||
/**
|
||||
* 单目距离估计(针孔模型):距离 = 焦距px × 参考体型 / 框高px。
|
||||
* 误差预期 ±30%(5~50m);生境区域按植被高度估算,均仅供参考。
|
||||
*/
|
||||
class DistanceEstimator(context: Context) {
|
||||
|
||||
private val cameraManager =
|
||||
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
|
||||
private val focalPxCache = HashMap<String, Float>()
|
||||
|
||||
// 参考体型(米)
|
||||
private val speciesSizeM = mapOf(
|
||||
"pheasant" to 0.45f, // 身高
|
||||
"cover" to 0.50f, // 植被高度(误差大)
|
||||
)
|
||||
|
||||
fun estimate(
|
||||
label: String,
|
||||
boxHeightNorm: Float,
|
||||
visibleHeightPx: Int,
|
||||
cameraId: String?,
|
||||
): Float? {
|
||||
val realH = speciesSizeM[label] ?: return null
|
||||
val boxH = boxHeightNorm * visibleHeightPx
|
||||
if (boxH < 8f) return null // 过小目标不估算
|
||||
val focalPx = focalPx(visibleHeightPx, cameraId)
|
||||
if (focalPx <= 0f) return null
|
||||
return (focalPx * realH / boxH).roundToInt().toFloat()
|
||||
}
|
||||
|
||||
/** focal_px = focal_mm × (imageHeightPx / sensorHeightMm);内参缺失时用视场角推算 */
|
||||
private fun focalPx(imageHeightPx: Int, cameraId: String?): Float {
|
||||
val key = "$cameraId:$imageHeightPx"
|
||||
focalPxCache[key]?.let { return it }
|
||||
val id = cameraId ?: return -1f
|
||||
val value = try {
|
||||
val c = cameraManager.getCameraCharacteristics(id)
|
||||
val focalMm = c.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
|
||||
?.firstOrNull()
|
||||
val sensor = c.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
|
||||
if (focalMm != null && sensor != null) {
|
||||
focalMm * imageHeightPx / sensor.height
|
||||
} else {
|
||||
fovFallback(imageHeightPx, c)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
-1f
|
||||
}
|
||||
focalPxCache[key] = value
|
||||
return value
|
||||
}
|
||||
|
||||
/** focal_px = imageHeightPx / (2·tan(fovV/2)) */
|
||||
private fun fovFallback(imageHeightPx: Int, c: CameraCharacteristics): Float {
|
||||
val fovV = c.get(CameraCharacteristics.LENS_INFO_AVAILABLE_VERTICAL_VIEW_ANGLES)
|
||||
?.firstOrNull() ?: return -1f
|
||||
return (imageHeightPx / (2.0 * tan(Math.toRadians(fovV / 2.0)))).toFloat()
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.example.observer.overlay
|
||||
|
||||
import android.graphics.Paint
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.observer.detection.CoordinateMapper
|
||||
import com.example.observer.detection.DetectionResult
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val speciesColors = mapOf(
|
||||
"pheasant" to Color(0xFFE53935),
|
||||
"cover" to Color(0xFFFDD835),
|
||||
)
|
||||
|
||||
private val speciesLabels = mapOf(
|
||||
"pheasant" to "野鸡",
|
||||
"cover" to "疑似区域",
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DetectionOverlay(
|
||||
results: List<DetectionResult>,
|
||||
rotation: Int,
|
||||
imageW: Int,
|
||||
imageH: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Canvas(modifier = modifier) {
|
||||
val viewW = size.width.toInt()
|
||||
val viewH = size.height.toInt()
|
||||
val textPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
textSize = 30f
|
||||
}
|
||||
results.forEach { r ->
|
||||
val rect = CoordinateMapper.mapToView(
|
||||
r.left, r.top, r.right, r.bottom,
|
||||
rotation, imageW, imageH, viewW, viewH,
|
||||
)
|
||||
val color = speciesColors[r.label] ?: Color.White
|
||||
val stroke = Stroke(
|
||||
width = 6.dp.toPx(),
|
||||
pathEffect = if (r.isHabitat) {
|
||||
PathEffect.dashPathEffect(floatArrayOf(20.dp.toPx(), 12.dp.toPx()))
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
drawRect(
|
||||
color = color,
|
||||
topLeft = Offset(rect.left, rect.top),
|
||||
size = Size(rect.width, rect.height),
|
||||
style = stroke,
|
||||
)
|
||||
textPaint.color = color.toArgb()
|
||||
val text = buildString {
|
||||
append(speciesLabels[r.label] ?: r.label)
|
||||
append(" ${(r.score * 100).toInt()}%")
|
||||
r.distanceM?.let { append(" · 约${it.roundToInt()}m") }
|
||||
}
|
||||
val labelTop = (rect.top - textPaint.textSize - 6).coerceAtLeast(0f)
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
text,
|
||||
rect.left.coerceAtLeast(0f) + 4f,
|
||||
labelTop + textPaint.textSize,
|
||||
textPaint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package com.example.observer.reminder
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.media.ToneGenerator
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
|
||||
class Reminder(context: Context) {
|
||||
|
||||
@Volatile var vibrateEnabled: Boolean = true
|
||||
|
||||
@Volatile var soundEnabled: Boolean = true
|
||||
|
||||
@Volatile var habitatAlertEnabled: Boolean = true
|
||||
|
||||
private val vibrator: Vibrator? =
|
||||
context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
|
||||
private val toneGenerator: ToneGenerator? = try {
|
||||
ToneGenerator(AudioManager.STREAM_NOTIFICATION, 60)
|
||||
} catch (e: RuntimeException) {
|
||||
null
|
||||
}
|
||||
|
||||
private var lastAlertLabel: String? = null
|
||||
private var lastAlertAt = 0L
|
||||
|
||||
/** 同类目标 10s 内只提醒一次;生境区域(cover)提醒方式与动物检测区分 */
|
||||
fun onDetected(label: String) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (label == lastAlertLabel && now - lastAlertAt < 10_000) return
|
||||
lastAlertAt = now
|
||||
lastAlertLabel = label
|
||||
|
||||
val isHabitat = label == "cover"
|
||||
if (isHabitat && !habitatAlertEnabled) return
|
||||
|
||||
if (vibrateEnabled) vibrate(isHabitat)
|
||||
if (soundEnabled) playTone(isHabitat)
|
||||
}
|
||||
|
||||
private fun vibrate(isHabitat: Boolean) {
|
||||
val v = vibrator ?: return
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
val effect = if (isHabitat) {
|
||||
VibrationEffect.createWaveform(longArrayOf(0, 150, 80, 150), -1)
|
||||
} else {
|
||||
VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE)
|
||||
}
|
||||
v.vibrate(effect)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
v.vibrate(if (isHabitat) longArrayOf(0, 150, 80, 150) else longArrayOf(0, 200), -1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun playTone(isHabitat: Boolean) {
|
||||
val tone = toneGenerator ?: return
|
||||
if (isHabitat) {
|
||||
// 双短音:生境预警
|
||||
tone.startTone(ToneGenerator.TONE_PROP_BEEP2, 120)
|
||||
tone.startTone(ToneGenerator.TONE_PROP_BEEP2, 120)
|
||||
} else {
|
||||
tone.startTone(ToneGenerator.TONE_PROP_BEEP2, 200)
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
toneGenerator?.release()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.example.observer.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.example.observer.ui.camera.CameraScreen
|
||||
import com.example.observer.ui.settings.SettingsScreen
|
||||
import com.example.observer.ui.theme.ObserverTheme
|
||||
|
||||
@Composable
|
||||
fun ObserverApp() {
|
||||
ObserverTheme {
|
||||
var showSettings by rememberSaveable { mutableStateOf(false) }
|
||||
if (showSettings) {
|
||||
SettingsScreen(onBack = { showSettings = false })
|
||||
} else {
|
||||
CameraScreen(onOpenSettings = { showSettings = true })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
package com.example.observer.ui.camera
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.observer.ObserverApp
|
||||
import com.example.observer.camera.CameraController
|
||||
import com.example.observer.camera.FrameAnalyzer
|
||||
import com.example.observer.camera.MotionDetector
|
||||
import com.example.observer.data.AppSettings
|
||||
import com.example.observer.data.DetectMode
|
||||
import com.example.observer.overlay.DetectionOverlay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Composable
|
||||
fun CameraScreen(onOpenSettings: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val container = (context.applicationContext as ObserverApp).container
|
||||
val viewModel: CameraViewModel = viewModel(
|
||||
factory = remember { CameraViewModelFactory(container) },
|
||||
)
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val settings by container.settingsRepository.settings.collectAsStateWithLifecycle(
|
||||
initialValue = AppSettings(),
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val executor = remember { Executors.newSingleThreadExecutor() }
|
||||
val previewView = remember {
|
||||
PreviewView(context).apply {
|
||||
scaleType = PreviewView.ScaleType.FIT_CENTER
|
||||
implementationMode = PreviewView.ImplementationMode.PERFORMANCE
|
||||
}
|
||||
}
|
||||
val cameraController = remember { CameraController(lifecycleOwner, executor) }
|
||||
val motionDetector = remember { MotionDetector() }
|
||||
val analyzer = remember(container.detector) {
|
||||
container.detector?.let { d ->
|
||||
FrameAnalyzer(d, motionDetector) { results, rotation, w, h, motion ->
|
||||
viewModel.onFramesAnalyzed(results, rotation, w, h, motion)
|
||||
}
|
||||
}
|
||||
}
|
||||
val interval by viewModel.analyzerIntervalMs.collectAsState()
|
||||
|
||||
var hasCameraPermission by remember {
|
||||
mutableStateOf(
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
||||
PackageManager.PERMISSION_GRANTED,
|
||||
)
|
||||
}
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
) { granted -> hasCameraPermission = granted }
|
||||
var retryKey by remember { mutableIntStateOf(0) }
|
||||
var initFailed by remember { mutableStateOf(false) }
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
cameraController.onCameraIdChanged = { viewModel.onCameraIdChanged(it) }
|
||||
onDispose { executor.shutdown() }
|
||||
}
|
||||
LaunchedEffect(interval) { analyzer?.intervalMs = interval }
|
||||
LaunchedEffect(retryKey) {
|
||||
cameraController.onInitError = { initFailed = true }
|
||||
if (hasCameraPermission) cameraController.start(previewView, analyzer)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (!hasCameraPermission) permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().background(Color.Black)) {
|
||||
if (hasCameraPermission) {
|
||||
AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize())
|
||||
|
||||
if (state.modelReady) {
|
||||
DetectionOverlay(
|
||||
results = state.results,
|
||||
rotation = state.rotation,
|
||||
imageW = state.imageWidthPx,
|
||||
imageH = state.imageHeightPx,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
"模型未加载:请将训练好的 model.tflite 放入 app/src/main/assets 后重新构建",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (initFailed) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.background(Color(0x99000000), RoundedCornerShape(12.dp))
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text("相机初始化失败", color = Color.White)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
TextButton(onClick = {
|
||||
initFailed = false
|
||||
retryKey++
|
||||
}) { Text("重试") }
|
||||
}
|
||||
}
|
||||
|
||||
CameraTopBar(
|
||||
detectMode = settings.detectMode,
|
||||
onCycleMode = {
|
||||
val next = DetectMode.entries[
|
||||
(settings.detectMode.ordinal + 1) % DetectMode.entries.size
|
||||
]
|
||||
scope.launch { container.settingsRepository.setDetectMode(next) }
|
||||
},
|
||||
onSwitchCamera = { cameraController.switchCamera(previewView, analyzer) },
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
)
|
||||
CameraBottomBar(
|
||||
vibrateEnabled = settings.vibrateEnabled,
|
||||
onToggleVibrate = {
|
||||
scope.launch {
|
||||
container.settingsRepository.setVibrateEnabled(!settings.vibrateEnabled)
|
||||
}
|
||||
},
|
||||
onOpenSettings = onOpenSettings,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
} else {
|
||||
PermissionGuide(
|
||||
onRequest = { permissionLauncher.launch(Manifest.permission.CAMERA) },
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CameraTopBar(
|
||||
detectMode: DetectMode,
|
||||
onCycleMode: () -> Unit,
|
||||
onSwitchCamera: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0x99000000))
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(onClick = onCycleMode) {
|
||||
Text("频率:${detectMode.label}", color = Color.White)
|
||||
}
|
||||
TextButton(onClick = onSwitchCamera) {
|
||||
Text("切换摄像头", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CameraBottomBar(
|
||||
vibrateEnabled: Boolean,
|
||||
onToggleVibrate: () -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0x99000000))
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(onClick = onToggleVibrate) {
|
||||
Text(if (vibrateEnabled) "提醒:开" else "提醒:关", color = Color.White)
|
||||
}
|
||||
TextButton(onClick = onOpenSettings) {
|
||||
Text("设置", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionGuide(onRequest: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
"需要相机权限才能进行实时识别",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(onClick = onRequest) { Text("授权相机") }
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package com.example.observer.ui.camera
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.observer.AppContainer
|
||||
import com.example.observer.data.DetectMode
|
||||
import com.example.observer.data.SettingsRepository
|
||||
import com.example.observer.detection.DetectionResult
|
||||
import com.example.observer.detection.MotionAggregator
|
||||
import com.example.observer.detection.MotionRegion
|
||||
import com.example.observer.detection.TFLiteDetector
|
||||
import com.example.observer.distance.DistanceEstimator
|
||||
import com.example.observer.reminder.Reminder
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.LinkedHashMap
|
||||
|
||||
data class CameraUiState(
|
||||
val modelReady: Boolean = false,
|
||||
val results: List<DetectionResult> = emptyList(),
|
||||
val rotation: Int = 90,
|
||||
val imageWidthPx: Int = 0,
|
||||
val imageHeightPx: Int = 0,
|
||||
)
|
||||
|
||||
class CameraViewModel(
|
||||
private val detector: TFLiteDetector?,
|
||||
private val distanceEstimator: DistanceEstimator,
|
||||
private val reminder: Reminder,
|
||||
settingsRepository: SettingsRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(CameraUiState(modelReady = detector != null))
|
||||
val state: StateFlow<CameraUiState> = _state.asStateFlow()
|
||||
|
||||
private val _analyzerIntervalMs = MutableStateFlow(DetectMode.CONTINUOUS.intervalMs)
|
||||
val analyzerIntervalMs: StateFlow<Long> = _analyzerIntervalMs.asStateFlow()
|
||||
|
||||
@Volatile
|
||||
private var showDistance = true
|
||||
|
||||
@Volatile
|
||||
private var confThreshold = 0.40f
|
||||
|
||||
@Volatile
|
||||
private var cameraId: String? = null
|
||||
|
||||
private val tracks = LinkedHashMap<String, Track>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.settings.collect { s ->
|
||||
detector?.confThreshold = s.confThreshold
|
||||
detector?.habitatThreshold = s.habitatThreshold
|
||||
reminder.vibrateEnabled = s.vibrateEnabled
|
||||
reminder.soundEnabled = s.soundEnabled
|
||||
reminder.habitatAlertEnabled = s.habitatAlertEnabled
|
||||
showDistance = s.showDistance
|
||||
confThreshold = s.confThreshold
|
||||
_analyzerIntervalMs.value = s.detectMode.intervalMs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCameraIdChanged(id: String) {
|
||||
cameraId = id
|
||||
}
|
||||
|
||||
/** 帧分析回调(分析线程调用,需线程安全) */
|
||||
fun onFramesAnalyzed(
|
||||
results: List<DetectionResult>,
|
||||
rotation: Int,
|
||||
imageWidthPx: Int,
|
||||
imageHeightPx: Int,
|
||||
motionRegions: List<MotionRegion>,
|
||||
) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val byKey = HashMap<String, DetectionResult>(results.size)
|
||||
results.forEach { byKey[keyOf(it)] = it }
|
||||
|
||||
synchronized(this) {
|
||||
// 过期目标清除(消失 2s 后移除)
|
||||
val expired = tracks.entries
|
||||
.filter { it.key !in byKey && now - it.value.lastSeenMs > 2000 }
|
||||
.map { it.key }
|
||||
expired.forEach { tracks.remove(it) }
|
||||
|
||||
byKey.forEach { (k, r) ->
|
||||
val t = tracks[k]
|
||||
tracks[k] = if (t == null) Track(now, now, r) else Track(t.firstSeenMs, now, r)
|
||||
}
|
||||
if (tracks.size > MAX_TRACKS) {
|
||||
val oldest = tracks.entries.sortedBy { it.value.lastSeenMs }.take(tracks.size - MAX_TRACKS)
|
||||
oldest.forEach { tracks.remove(it.key) }
|
||||
}
|
||||
|
||||
// 目标停留 ≥ 0.5s 才展示;消失 2s 内不闪断
|
||||
val visible = tracks.values
|
||||
.filter { now - it.firstSeenMs >= 500 && now - it.lastSeenMs <= 2000 }
|
||||
.filter { shouldShow(it.result, motionRegions) }
|
||||
.map { t ->
|
||||
val r = t.result
|
||||
val boosted = r.score < confThreshold &&
|
||||
motionRegions.any { MotionAggregator.centerInRegion(r, it) }
|
||||
val distance = if (showDistance) {
|
||||
// 模型 CENTER_CROP 到方形输入, 归一化框高对应原图较短边(最大内接正方形)
|
||||
distanceEstimator.estimate(
|
||||
r.label, r.height, minOf(imageWidthPx, imageHeightPx), cameraId,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
r.copy(
|
||||
score = if (boosted) minOf(r.score + MOTION_BOOST, 1f) else r.score,
|
||||
distanceM = distance,
|
||||
)
|
||||
}
|
||||
|
||||
// 提醒:刚变为可见的目标(防重复由 Reminder 按类别 10s 控制)
|
||||
tracks.values.forEach { t ->
|
||||
if (!shouldShow(t.result, motionRegions)) return@forEach
|
||||
val age = now - t.firstSeenMs
|
||||
if (age in 500..2100 && now - t.lastSeenMs <= 300) {
|
||||
reminder.onDetected(t.result.label)
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
results = visible,
|
||||
rotation = rotation,
|
||||
imageWidthPx = imageWidthPx,
|
||||
imageHeightPx = imageHeightPx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示判定:cover 已按生境阈值过滤;动物分低于阈值时,
|
||||
* 仅当与运动区域重叠(低分候选)才提升显示。
|
||||
*/
|
||||
private fun shouldShow(r: DetectionResult, motionRegions: List<MotionRegion>): Boolean {
|
||||
if (r.isHabitat) return true
|
||||
if (r.score >= confThreshold) return true
|
||||
if (r.score < TFLiteDetector.MIN_SCORE) return false
|
||||
return motionRegions.any { MotionAggregator.centerInRegion(r, it) }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
reminder.release()
|
||||
}
|
||||
|
||||
private fun keyOf(r: DetectionResult): String =
|
||||
"${r.label}:${(r.centerX * 10).toInt()}:${(r.centerY * 10).toInt()}"
|
||||
|
||||
private data class Track(
|
||||
val firstSeenMs: Long,
|
||||
val lastSeenMs: Long,
|
||||
val result: DetectionResult,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val MAX_TRACKS = 30
|
||||
|
||||
/** 低分目标与运动区域重叠时的置信度提升量 */
|
||||
private const val MOTION_BOOST = 0.15f
|
||||
}
|
||||
}
|
||||
|
||||
class CameraViewModelFactory(private val container: AppContainer) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(CameraViewModel::class.java)) {
|
||||
return CameraViewModel(
|
||||
detector = container.detector as? TFLiteDetector,
|
||||
distanceEstimator = container.distanceEstimator,
|
||||
reminder = container.reminder,
|
||||
settingsRepository = container.settingsRepository,
|
||||
) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package com.example.observer.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.example.observer.ObserverApp
|
||||
import com.example.observer.data.AppSettings
|
||||
import com.example.observer.data.DetectMode
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(onBack: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val repository = (context.applicationContext as ObserverApp).container.settingsRepository
|
||||
val settings by repository.settings.collectAsStateWithLifecycle(initialValue = AppSettings())
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("设置") },
|
||||
navigationIcon = {
|
||||
TextButton(onClick = onBack) { Text("返回") }
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text("动物识别阈值:${"%.2f".format(settings.confThreshold)}")
|
||||
Slider(
|
||||
value = settings.confThreshold,
|
||||
onValueChange = { scope.launch { repository.setConfThreshold(it) } },
|
||||
valueRange = 0.2f..0.7f,
|
||||
)
|
||||
|
||||
Text("生境区域阈值:${"%.2f".format(settings.habitatThreshold)}")
|
||||
Slider(
|
||||
value = settings.habitatThreshold,
|
||||
onValueChange = { scope.launch { repository.setHabitatThreshold(it) } },
|
||||
valueRange = 0.2f..0.7f,
|
||||
)
|
||||
|
||||
Text("检测频率")
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
DetectMode.entries.forEach { mode ->
|
||||
FilterChip(
|
||||
selected = settings.detectMode == mode,
|
||||
onClick = { scope.launch { repository.setDetectMode(mode) } },
|
||||
label = { Text(mode.label) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow("震动提醒", settings.vibrateEnabled) {
|
||||
scope.launch { repository.setVibrateEnabled(it) }
|
||||
}
|
||||
SwitchRow("提示音", settings.soundEnabled) {
|
||||
scope.launch { repository.setSoundEnabled(it) }
|
||||
}
|
||||
SwitchRow("距离标注", settings.showDistance) {
|
||||
scope.launch { repository.setShowDistance(it) }
|
||||
}
|
||||
SwitchRow("生境区域预警", settings.habitatAlertEnabled) {
|
||||
scope.launch { repository.setHabitatAlertEnabled(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwitchRow(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(title)
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.example.observer.ui.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val DarkColors = darkColorScheme(
|
||||
primary = Color(0xFF66BB6A),
|
||||
onPrimary = Color(0xFF00391F),
|
||||
secondary = Color(0xFF80CBC4),
|
||||
surface = Color(0xFF1C1C1C),
|
||||
background = Color(0xFF101010),
|
||||
onBackground = Color(0xFFE4E4E4),
|
||||
onSurface = Color(0xFFE4E4E4),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ObserverTheme(content: @Composable () -> Unit) {
|
||||
MaterialTheme(colorScheme = DarkColors, content = content)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#1B5E20"
|
||||
android:pathData="M54,54m-54,0a54,54 0,1 1,108 0a54,54 0,1 1,-108 0" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="6"
|
||||
android:pathData="M54,54m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0" />
|
||||
<path
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="4"
|
||||
android:pathData="M54,22 L54,34 M54,74 L54,86 M22,54 L34,54 M74,54 L86,54" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,54m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0" />
|
||||
</vector>
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">野视</string>
|
||||
</resources>
|
||||
@@ -1,7 +0,0 @@
|
||||
<resources>
|
||||
<style name="Theme.Observer" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/black</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class CoordinateMapperTest {
|
||||
|
||||
@Test
|
||||
fun rotation90_center_mapsToViewCenter() {
|
||||
// 横屏图像 1280x720,旋转 90 后竖屏显示 720x1280,视图 1080x1920(等比,无偏移)
|
||||
val rect = CoordinateMapper.mapToView(
|
||||
normLeft = 0.4f, normTop = 0.4f, normRight = 0.6f, normBottom = 0.6f,
|
||||
rotation = 90, imageW = 1280, imageH = 720,
|
||||
viewW = 1080, viewH = 1920,
|
||||
)
|
||||
assertEquals(540f, rect.centerX, 1f)
|
||||
assertEquals(960f, rect.centerY, 1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rotation90_corner_appliesFitCenterOffset() {
|
||||
// 视图 1080x2400,竖屏图像 720x1280,scale=1.5,纵向偏移 (2400-1920)/2=240
|
||||
val rect = CoordinateMapper.mapToView(
|
||||
normLeft = 0f, normTop = 0f, normRight = 0.5f, normBottom = 0.5f,
|
||||
rotation = 90, imageW = 1280, imageH = 720,
|
||||
viewW = 1080, viewH = 2400,
|
||||
)
|
||||
// 旋转后:x0=1-bottom=0.5, y0=left=0, x1=1-top=1, y1=right=0.5
|
||||
assertEquals(540f, rect.left, 1f) // 0.5 * 720 * 1.5
|
||||
assertEquals(240f, rect.top, 1f) // 0 * 1280 * 1.5 + 240
|
||||
assertEquals(1080f, rect.right, 1f) // 1 * 720 * 1.5
|
||||
assertEquals(1200f, rect.bottom, 1f) // 0.5 * 1280 * 1.5 + 240
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rotation0_keepsCoordinates() {
|
||||
val rect = CoordinateMapper.mapToView(
|
||||
normLeft = 0.2f, normTop = 0.3f, normRight = 0.5f, normBottom = 0.7f,
|
||||
rotation = 0, imageW = 1080, imageH = 1920,
|
||||
viewW = 1080, viewH = 1920,
|
||||
)
|
||||
assertEquals(216f, rect.left, 1f)
|
||||
assertEquals(576f, rect.top, 1f)
|
||||
assertEquals(540f, rect.right, 1f)
|
||||
assertEquals(1344f, rect.bottom, 1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rotation180_flipsBothAxes() {
|
||||
val rect = CoordinateMapper.mapToView(
|
||||
normLeft = 0.2f, normTop = 0.2f, normRight = 0.4f, normBottom = 0.4f,
|
||||
rotation = 180, imageW = 1080, imageH = 1920,
|
||||
viewW = 1080, viewH = 1920,
|
||||
)
|
||||
// 翻转:x'=1-x, y'=1-y
|
||||
assertEquals(648f, rect.left, 1f) // (1-0.4) * 1080
|
||||
assertEquals(1152f, rect.top, 1f) // (1-0.4) * 1920
|
||||
assertEquals(864f, rect.right, 1f) // (1-0.2) * 1080
|
||||
assertEquals(1536f, rect.bottom, 1f) // (1-0.2) * 1920
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class MotionAggregatorTest {
|
||||
|
||||
// 96x64 图,8x8 块 → 每块 12x8 像素
|
||||
|
||||
@Test
|
||||
fun noMotion_returnsEmpty() {
|
||||
val diff = IntArray(96 * 64)
|
||||
assertTrue(MotionAggregator.aggregate(diff, 96, 64).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleBlockMotion_detectsRegion() {
|
||||
val diff = IntArray(96 * 64)
|
||||
// 块 (2,3):x 24..35, y 24..31,全部置 1
|
||||
for (y in 24 until 32) {
|
||||
for (x in 24 until 36) diff[y * 96 + x] = 1
|
||||
}
|
||||
val regions = MotionAggregator.aggregate(diff, 96, 64)
|
||||
assertEquals(1, regions.size)
|
||||
val r = regions[0]
|
||||
assertTrue(r.left <= 24f / 96f && r.right >= 36f / 96f)
|
||||
assertTrue(r.top <= 24f / 64f && r.bottom >= 32f / 64f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoSeparateMotions_detectsTwoRegions() {
|
||||
val diff = IntArray(96 * 64)
|
||||
for (y in 0 until 8) for (x in 0 until 12) diff[y * 96 + x] = 1
|
||||
for (y in 48 until 64) for (x in 72 until 96) diff[y * 96 + x] = 1
|
||||
val regions = MotionAggregator.aggregate(diff, 96, 64)
|
||||
assertEquals(2, regions.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun globalNoise_filteredOut() {
|
||||
val diff = IntArray(96 * 64)
|
||||
// 全屏散点噪声(随机块),但无连续连通域
|
||||
val rnd = java.util.Random(42)
|
||||
for (i in diff.indices) if (rnd.nextFloat() < 0.1f) diff[i] = 1
|
||||
val regions = MotionAggregator.aggregate(diff, 96, 64)
|
||||
assertTrue(regions.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun centerInRegion_matches() {
|
||||
val box = DetectionResult(
|
||||
label = "hare",
|
||||
score = 0.30f,
|
||||
left = 0.2f,
|
||||
top = 0.3f,
|
||||
right = 0.4f,
|
||||
bottom = 0.5f,
|
||||
)
|
||||
val region = MotionRegion(0.1f, 0.2f, 0.5f, 0.6f)
|
||||
assertTrue(MotionAggregator.centerInRegion(box, region))
|
||||
val outside = MotionRegion(0.6f, 0.7f, 0.9f, 0.9f)
|
||||
assertFalse(MotionAggregator.centerInRegion(box, outside))
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.example.observer.detection
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class NmsTest {
|
||||
|
||||
private fun box(l: Float, t: Float, r: Float, b: Float, score: Float, label: String = "x") =
|
||||
DetectionResult(label, score, l, t, r, b)
|
||||
|
||||
@Test
|
||||
fun overlappingBoxes_keepHighestScore() {
|
||||
val a = box(0.1f, 0.1f, 0.5f, 0.5f, 0.8f)
|
||||
val b = box(0.12f, 0.12f, 0.52f, 0.52f, 0.6f)
|
||||
val result = nms(listOf(a, b), 0.45f)
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(0.8f, result[0].score, 1e-6f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun separateBoxes_bothKept() {
|
||||
val a = box(0.1f, 0.1f, 0.3f, 0.3f, 0.8f)
|
||||
val b = box(0.7f, 0.7f, 0.9f, 0.9f, 0.6f)
|
||||
val result = nms(listOf(a, b), 0.45f)
|
||||
assertEquals(2, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun lowScoreBox_suppressedByHigherScore() {
|
||||
val a = box(0.1f, 0.1f, 0.5f, 0.5f, 0.9f)
|
||||
val b = box(0.1f, 0.1f, 0.5f, 0.5f, 0.5f)
|
||||
val result = nms(listOf(b, a), 0.45f)
|
||||
assertEquals(1, result.size)
|
||||
assertTrue(result[0].score > 0.5f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iou_nonOverlapping_isZero() {
|
||||
val a = box(0.0f, 0.0f, 0.2f, 0.2f, 1f)
|
||||
val b = box(0.8f, 0.8f, 1f, 1f, 1f)
|
||||
assertEquals(0f, iou(a, b), 1e-6f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun iou_identicalBoxes_isOne() {
|
||||
val a = box(0.1f, 0.1f, 0.5f, 0.5f, 1f)
|
||||
assertEquals(1f, iou(a, a), 1e-6f)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
# Observer(野视)· 野生动物实时识别 Android App — 技术方案
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档版本 | v1.1 |
|
||||
| 编写日期 | 2026-08-17 |
|
||||
| 状态 | 初稿(v1.1:确认功能范围为"仅实时识别",移除拍照留存) |
|
||||
| 适用产品 | 纯 Android 原生 App |
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 项目背景
|
||||
|
||||
野生动物观察爱好者在户外需要一款便携工具:打开手机相机,即可从实时画面中识别野鸡,通过检测框与提醒辅助快速发现。
|
||||
|
||||
### 1.2 项目目标
|
||||
|
||||
- 端侧实时目标检测,**全程离线可用**,不依赖网络;
|
||||
- 野鸡实时框选识别,展示类别、置信度与**大致距离**;
|
||||
- 检测到目标时通过震动 / 声音提醒用户,辅助快速发现;
|
||||
- 纯 Android 原生 App(Kotlin),兼容 Android 7.0+ 主流机型,中端机流畅运行;
|
||||
- **不做拍照、记录、统计等留存功能**,专注实时识别这一核心体验。
|
||||
|
||||
### 1.3 名词术语
|
||||
|
||||
| 术语 | 说明 |
|
||||
| --- | --- |
|
||||
| ImageAnalysis | CameraX 的帧分析用例,用于逐帧回调图像数据 |
|
||||
| TFLite | TensorFlow Lite,Google 端侧推理框架 |
|
||||
| YOLO | You Only Look Once,单阶段目标检测算法 |
|
||||
| NMS | Non-Maximum Suppression,非极大值抑制 |
|
||||
| GPU Delegate | TFLite 的 GPU 加速委托,将算子下发 GPU 执行 |
|
||||
| mAP | mean Average Precision,目标检测平均精度指标 |
|
||||
| IoU | Intersection over Union,交并比 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 需求概述
|
||||
|
||||
### 2.1 目标用户
|
||||
|
||||
| 用户群 | 典型诉求 |
|
||||
| --- | --- |
|
||||
| 野生动物观察爱好者 | 户外实时识别画面中的野鸡,不惊扰、近距离观察 |
|
||||
| 户外徒步 / 摄影人群 | 快速发现野鸡,辅助取景构图 |
|
||||
| 自然教育、科普工作者 | 物种识别辅助教学 |
|
||||
|
||||
### 2.2 核心场景
|
||||
|
||||
| 编号 | 场景 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| S1 | 野外实时识别 | 徒步 / 观鸟时打开相机,实时框选画面中的野鸡,显示类别、置信度与大致距离 |
|
||||
| S2 | 快速扫视寻找 | 移动取景快速扫视,检测到野鸡立即震动 / 声音提醒,无需停留操作 |
|
||||
|
||||
### 2.3 功能需求摘要
|
||||
|
||||
实时识别(P0)、识别结果叠加展示(P0)、检测提醒(P1)、生境区域预警(P1)、设置(P1)、合规提示(P0)、低光增强(P2)。详见《项目功能文档》。
|
||||
|
||||
### 2.4 非功能需求摘要
|
||||
|
||||
| 指标 | 目标 |
|
||||
| --- | --- |
|
||||
| 识别延迟 | 中端机单帧 ≤ 80ms |
|
||||
| 预览流畅度 | 连续模式 2~3 帧检测一次,预览不卡顿 |
|
||||
| 耗电 | 连续使用 1 小时耗电 ≤ 15% |
|
||||
| 兼容性 | Android 7.0+(minSdk 24),覆盖主流国产机与三星 |
|
||||
| 稳定性 | 崩溃率 ≤ 0.5%,启动成功率 ≥ 99% |
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
### 3.1 架构图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph UI层["UI 层 · Jetpack Compose"]
|
||||
MainScreen["相机主界面<br/>预览 + 检测叠加层"]
|
||||
SettingsScreen["设置界面"]
|
||||
end
|
||||
|
||||
subgraph ViewModel层["ViewModel 层"]
|
||||
CameraViewModel["CameraViewModel"]
|
||||
SettingsViewModel["SettingsViewModel"]
|
||||
end
|
||||
|
||||
subgraph 领域层["领域层"]
|
||||
Detector接口["Detector 接口"]
|
||||
end
|
||||
|
||||
subgraph 基础设施层["基础设施层"]
|
||||
CameraX["CameraX<br/>Preview / ImageAnalysis"]
|
||||
TFLite["TFLite 推理引擎<br/>YOLOv8n 模型 + GPU Delegate"]
|
||||
DataStore["DataStore 设置存储"]
|
||||
end
|
||||
|
||||
UI层 --> ViewModel层
|
||||
ViewModel层 --> 领域层
|
||||
领域层 --> 基础设施层
|
||||
```
|
||||
|
||||
### 3.2 分层说明
|
||||
|
||||
| 层 | 职责 |
|
||||
| --- | --- |
|
||||
| UI 层 | Compose 页面渲染、检测叠加层绘制、交互 |
|
||||
| ViewModel 层 | 页面状态管理、识别结果分发、提醒触发 |
|
||||
| 领域层 | 检测器抽象接口 |
|
||||
| 基础设施层 | CameraX、TFLite、DataStore 等能力实现 |
|
||||
|
||||
### 3.3 核心数据流(识别链路)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["CameraX 相机帧"] --> B["ImageAnalysis 帧分析"]
|
||||
B --> C["预处理<br/>缩放 320×320 / 归一化 / 旋转校正"]
|
||||
C --> D["TFLite 推理<br/>YOLOv8n"]
|
||||
D --> E["后处理<br/>解码 / NMS / 阈值过滤"]
|
||||
E --> F["叠加层渲染<br/>边框 + 类别 + 置信度"]
|
||||
F --> G["UI 展示"]
|
||||
E --> H["结果提醒<br/>震动 / 提示音"]
|
||||
```
|
||||
|
||||
### 3.4 部署形态
|
||||
|
||||
- **本地优先**:识别全部离线完成,运行期无网络请求;
|
||||
- **云端(V2 可选)**:仅用于模型版本更新下发。
|
||||
|
||||
---
|
||||
|
||||
## 4. 技术选型
|
||||
|
||||
| 类别 | 选型 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| 开发语言 | Kotlin | Android 官方推荐,协程生态成熟 |
|
||||
| UI 框架 | Jetpack Compose(Material 3) | 声明式 UI,开发效率高,叠加层绘制灵活 |
|
||||
| 相机 | CameraX(Preview / ImageAnalysis) | Jetpack 官方库,生命周期安全,机型兼容性最佳 |
|
||||
| 目标检测模型 | YOLOv8n(自定义 4 类训练) | 精度/速度均衡,端侧部署方案成熟 |
|
||||
| 推理框架 | TensorFlow Lite 2.16+(GPU Delegate) | 官方支持 GPU 加速;备选 NCNN / MNN |
|
||||
| 配置存储 | DataStore Preferences | 阈值、开关等设置 |
|
||||
| 异步 | Kotlin Coroutines + Flow | 主线程安全,生命周期感知 |
|
||||
| 构建 | Gradle(Kotlin DSL)+ AGP 8.x | 现代构建配置 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心功能技术方案
|
||||
|
||||
### 5.1 实时识别管线
|
||||
|
||||
- CameraX 组合:`Preview`(取景)+ `ImageAnalysis`(识别);
|
||||
- `ImageAnalysis` 使用 `STRATEGY_KEEP_ONLY_LATEST` 背压策略,保证不积压帧;
|
||||
- 输出格式使用 `OUTPUT_IMAGE_FORMAT_RGBA_8888`(CameraX 1.3+),免去 YUV→RGB 手动转换;
|
||||
- **帧节流**:连续模式每 2~3 帧检测一次;标准模式 300ms 一次;省电模式每秒一次(依据设置);
|
||||
- 推理在独立检测线程执行,单例互斥锁防止重叠推理;分析线程不阻塞主线程。
|
||||
|
||||
### 5.2 检测叠加层
|
||||
|
||||
- Compose Canvas 绘制检测框(矩形 + 类别标签 + 置信度),颜色按类别区分;
|
||||
- 检测框内同时标注**大致距离**(如"约 25m"),随检测框实时更新;
|
||||
- 距离标注与检测框同生共灭,展示逻辑一致(≥ 0.5s 出现、消失 2s 后移除);
|
||||
- 坐标映射链路:模型归一化坐标 → 旋转校正(sensor rotation)→ 预览视图坐标(含 FIT_CENTER 裁剪偏移修正);
|
||||
- 目标保持 ≥ 0.5s 才展示,避免单帧误检闪烁。
|
||||
|
||||
### 5.3 距离标注(单目估计)
|
||||
|
||||
- 采用单目针孔模型:`距离 ≈ 焦距px × 物种参考体型 / 检测框像素高度`;
|
||||
- 焦距 px 由相机内参换算(`LENS_INFO_AVAILABLE_FOCAL_LENGTHS` × `SENSOR_INFO_PHYSICAL_SIZE`),个别机型回退用视场角推算;
|
||||
- 物种参考体型内置表(见 6.1),每类一个平均体型值;
|
||||
- 展示为"约 X m";5~50m 范围误差预期 ≤ ±30%;
|
||||
- 纯算术运算,无额外模型与算力开销;V2 可引入地面平面法(设备俯仰角 + 相机高度)提升精度。
|
||||
|
||||
### 5.4 检测提醒
|
||||
|
||||
- 检测到目标时触发震动 / 提示音,用户可开关;
|
||||
- 防重复打扰:10s 内同类目标只提醒一次;
|
||||
- 目标消失后 2s 内不闪断显示,避免频繁提醒。
|
||||
|
||||
### 5.5 低光增强(P2)
|
||||
|
||||
- 简单直方图均衡 / 亮度增益提升低光帧可见度;
|
||||
- V2 可引入夜视增强网络思路。
|
||||
|
||||
---
|
||||
|
||||
## 6. AI 模型方案(核心)
|
||||
|
||||
### 6.1 目标类别定义
|
||||
|
||||
| 类别 ID | 英文标签 | 中文名 | 涵盖范围 | 参考体型(距离估计用) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 0 | pheasant | 野鸡 | 环颈雉等雉类 | ≈ 0.45m(身高) |
|
||||
| 1 | cover | 生境区域 | 草丛 / 灌木 / 水面等疑似生境(黄色虚线框预警,F08) | ≈ 0.5m(参考植被高度) |
|
||||
|
||||
### 6.2 模型选型对比
|
||||
|
||||
| 模型 | 输入尺寸 | 中端机单帧耗时(GPU) | 精度 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **YOLOv8n(推荐)** | 320~640 | 约 30~80ms | 高 | 支持自定义类别训练,精度/速度平衡 |
|
||||
| EfficientDet-Lite0/2 | 320 | 约 20~50ms | 中 | 仅 COCO 预置类别 |
|
||||
| YOLOv5s | 640 | 约 80~150ms | 高 | 体积与耗电偏大 |
|
||||
| SSD MobileNetV2 | 300 | 约 15~30ms | 低 | 小目标与远距离效果差 |
|
||||
|
||||
**结论**:选用 **YOLOv8n**,使用预训练权重迁移学习自定义训练,导出 TFLite 端侧部署。
|
||||
|
||||
### 6.3 数据集方案
|
||||
|
||||
| 项 | 方案 |
|
||||
| --- | --- |
|
||||
| 数据量 | 1000~2000 张(首版可 500+ 起步,滚动补充) |
|
||||
| 多样性 | 覆盖不同季节、晨昏/正午/逆光、远近距离、姿态、遮挡、背景(草丛/农田/林地/雪地) |
|
||||
| 标注 | 人工标注(LabelImg 等工具),YOLO 格式(class, cx, cy, w, h) |
|
||||
| 生境标注 | 人工标注可疑度最高的藏身点(cover 类,黄色框,约占画面 2%~15%),综合植被密度 / 地形 / 光线判断 |
|
||||
| 数据增强 | Mosaic、MixUp、HSV 扰动、随机翻转、随机缩放裁剪 |
|
||||
| 数据划分 | train 80% / val 10% / test 10% |
|
||||
| 负样本 | 补充无目标场景图,控制误检 |
|
||||
|
||||
### 6.4 训练方案
|
||||
|
||||
- 框架:ultralytics YOLOv8,预训练权重 `yolov8n.pt` 迁移学习;
|
||||
- 超参:imgsz=640(训练)、epochs 100~200(早停)、batch 16~32(视 GPU 而定);
|
||||
- 评估指标:
|
||||
- mAP@0.5 ≥ **0.85**(达标线 0.80);
|
||||
- mAP@0.5:0.95 ≥ **0.55**;
|
||||
- 负样本误检率 ≤ **2%**;
|
||||
- 远距离小目标(高度 ≤ 20px)召回率 ≥ **60%**。
|
||||
|
||||
### 6.5 模型转换与量化
|
||||
|
||||
- 导出命令:`yolo export model=best.pt format=tflite imgsz=320`;
|
||||
- 量化策略:
|
||||
- 优先 **fp16**(配合 GPU Delegate,精度损失小);
|
||||
- 低端机 / CPU 场景使用 **int8** 量化(体积更小、CPU 更快);
|
||||
- 输入尺寸:320×320(连续检测)/ 416×416(远距离模式,V2)。
|
||||
|
||||
### 6.6 推理优化
|
||||
|
||||
- GPU Delegate 优先,初始化失败自动回退 CPU(NNAPI 可选);
|
||||
- 推理线程 4 线程,首次推理前执行预热(dummy run);
|
||||
- 输入输出 ByteBuffer / Bitmap 复用,避免热路径频繁分配。
|
||||
|
||||
### 6.7 精度与速度目标
|
||||
|
||||
| 指标 | 目标 |
|
||||
| --- | --- |
|
||||
| 检测阈值(默认) | confidence ≥ 0.40,IoU-NMS = 0.45 |
|
||||
| 单帧推理延迟 | ≤ 80ms(中端机,320 输入,GPU) |
|
||||
| 模型体积 | fp16 ≤ 8MB,int8 ≤ 4MB |
|
||||
| 识别类别数 | 2 类(野鸡 + 生境区域 cover) |
|
||||
| 距离标注 | "约 X m",5~50m 误差 ≤ ±30%,计算开销可忽略 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 性能指标与优化
|
||||
|
||||
| 指标 | 目标值 | 主要优化手段 |
|
||||
| --- | --- | --- |
|
||||
| 启动到相机预览 | ≤ 1.5s | 启动即初始化 CameraProvider,懒加载非核心模块 |
|
||||
| 单帧检测延迟 | ≤ 80ms | GPU Delegate、320 输入、线程复用 |
|
||||
| 预览帧率 | ≥ 25fps | 帧节流、KEEP_ONLY_LATEST、避免主线程工作 |
|
||||
| 内存峰值 | ≤ 200MB | Bitmap/ByteBuffer 复用,无大对象常驻 |
|
||||
| 连续 1 小时耗电 | ≤ 15% | 检测帧节流、省电模式、后台自动释放相机 |
|
||||
| App 体积 | ≤ 40MB | ABI 拆分、模型量化、R8 混淆 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 数据与隐私
|
||||
|
||||
- 无账号、无埋点、无广告 SDK,**不采集任何用户数据**;
|
||||
- 不保存照片、不记录位置,运行期无网络请求;
|
||||
- 模型与标签随 APK 打包,识别数据仅存在于内存中,随进程结束自动释放。
|
||||
|
||||
---
|
||||
|
||||
## 9. 安全与合规
|
||||
|
||||
- 产品定位为**野生动物观察、识别工具**,不提供任何猎捕、诱捕、伤害野生动物的功能或指导;
|
||||
- 遵守《中华人民共和国野生动物保护法》《陆生野生动物保护实施条例》等法律法规;
|
||||
- 野鸡(雉类)属"三有"保护动物,猎捕须依法取得许可,App 不鼓励、不协助非法猎捕;
|
||||
- App 内置观察伦理提示:保持安全距离、不惊扰动物、遵守保护区管理规定(首次启动展示,功能文档 F07)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 开发计划(里程碑)
|
||||
|
||||
| 阶段 | 周期 | 交付内容 |
|
||||
| --- | --- | --- |
|
||||
| M0 准备 | 1 周 | 需求确认、数据集启动采集、Android 工程脚手架、模型基线 |
|
||||
| M1 MVP | 3~4 周 | 相机预览 + 实时检测 + 叠加层 + 检测提醒 + 设置 + 合规提示 |
|
||||
| M2 打磨发布 | 1~2 周 | 性能优化、真机矩阵测试、混淆加固、上架准备 |
|
||||
|
||||
总计约 **5~7 周**(不含数据采集并行时间)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对 |
|
||||
| --- | --- | --- |
|
||||
| 训练数据不足 / 类别相近 | 精度低、误检 | 持续滚动采集数据;增加难例挖掘;灰度发布迭代模型 |
|
||||
| 小目标(远处动物) | 漏检 | 提高输入分辨率;帧节流换取算力;提示用户靠近/变焦;V2 引入 SAHI/tiling 或专用小目标模型 |
|
||||
| 低光 / 夜间场景 | 漏检 | 低光增强预处理;V2 夜视模式 |
|
||||
| 单目距离估计精度有限 | 距离显示不准 | 标注为"约"并明示误差预期;体型表持续校准;V2 地面平面法 + 水下折射修正 |
|
||||
| 生境区域误报偏多 | 提醒频繁、体验差 | 独立低阈值 + 区分提醒方式 + 防重复机制;生境阈值可调;模型迭代降低误报 |
|
||||
| 中低端机型性能不足 | 帧率低、发热 | 动态分辨率、降频检测、GPU 回退策略、省电模式 |
|
||||
| CameraX 个别机型异常 | 黑屏/闪退 | 机型兼容测试矩阵、崩溃监控、失败回退(重试/默认配置) |
|
||||
| 合规风险(被用于非法猎捕) | 法律风险 | 产品定位为观察工具、内置合规提示、不提供猎捕辅助功能 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 验收标准
|
||||
|
||||
1. 野鸡测试集 mAP@0.5 ≥ 0.85,负样本误检率 ≤ 2%;
|
||||
2. 中端机(如骁龙 7 系)单帧检测 ≤ 80ms,预览流畅无卡顿;
|
||||
3. 连续使用 1 小时耗电 ≤ 15%,无异常发热;
|
||||
4. 主流机型(小米 / 华为 / OPPO / vivo / 三星,各 ≥ 2 台)启动成功率 ≥ 99%,崩溃率 ≤ 0.5%;
|
||||
5. 飞行模式下实时识别全流程(预览、检测、提醒、设置)可用;
|
||||
6. 检测框正确显示类别、置信度与距离;5~50m 范围距离误差 ≤ ±30%;
|
||||
7. 通过应用商店合规审核(隐私政策、权限声明完整)。
|
||||
@@ -1,193 +0,0 @@
|
||||
# Observer(野视)· 项目功能文档
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档版本 | v1.1 |
|
||||
| 编写日期 | 2026-08-17 |
|
||||
| 状态 | 初稿(v1.1:确认功能范围为"仅实时识别",移除拍照留存) |
|
||||
| 配套文档 | 《01-技术方案》《03-技术实现文档》 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 产品概述
|
||||
|
||||
### 1.1 产品定位
|
||||
|
||||
一款**纯 Android 原生**的野生动物实时识别 App。用户打开相机,即可从实时画面中识别野鸡,通过彩色检测框(含类别、置信度、距离标注)与震动 / 声音提醒辅助快速发现。**无拍照、无记录、无统计**,专注实时识别这一核心体验,**全程离线可用**。
|
||||
|
||||
### 1.2 目标用户
|
||||
|
||||
- 野生动物观察爱好者(观鸟 / 观兽);
|
||||
- 户外徒步、摄影人群;
|
||||
- 自然教育、科普工作者。
|
||||
|
||||
### 1.3 价值主张
|
||||
|
||||
- **即时发现**:打开相机即识别,检测到目标立即提醒,免去翻图鉴、猜物种;
|
||||
- **零门槛**:不依赖网络,不依赖外设,一部手机即可;
|
||||
- **纯净体验**:无留存功能、无账号、无广告,即开即用。
|
||||
|
||||
---
|
||||
|
||||
## 2. 功能架构(功能树)
|
||||
|
||||
```
|
||||
Observer
|
||||
├── 实时识别
|
||||
│ ├── 相机实时预览(F01)
|
||||
│ ├── 目标实时检测(F02)
|
||||
│ ├── 识别结果展示与距离标注(F03)
|
||||
│ ├── 检测提醒(F04)
|
||||
│ └── 低光增强(F06,P2)
|
||||
├── 设置
|
||||
│ ├── 识别参数(F05)
|
||||
│ └── 提醒设置(F04 子项)
|
||||
└── 合规提示(F07)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 功能需求清单
|
||||
|
||||
优先级定义:**P0**=必须,**P1**=V1.0,**P2**=后续版本。
|
||||
|
||||
| 编号 | 功能 | 功能描述 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| F01 | 相机实时预览 | 全屏取景,默认后置,支持自动对焦、双指缩放、点击对焦、前后摄切换 | P0 |
|
||||
| F02 | 目标实时检测 | 对野鸡实时检测框选 | P0 |
|
||||
| F03 | 识别结果展示与距离标注 | 检测框 + 类别名 + 置信度百分比 + 大致距离(约 X m);野鸡红色实线框,生境区域黄色虚线框 | P0 |
|
||||
| F04 | 检测提醒 | 检测到目标时震动 / 提示音;支持开关与 10s 防重复 | P1 |
|
||||
| F05 | 设置 | 置信度阈值(0.2~0.7)、检测频率(连续 / 标准 / 省电)、提醒开关、低光增强开关 | P1 |
|
||||
| F06 | 低光增强 | 低光环境下自动亮度 / 对比度增强,提高识别率 | P2 |
|
||||
| F07 | 合规提示 | 首次启动展示《观察伦理与法律提示》,需用户确认 | P0 |
|
||||
| F08 | 生境区域预警 | 画面中未检测到目标时,识别草丛 / 灌木 / 水面等疑似生境区域,以黄色虚线框展示(含距离标注)并触发预警(独立阈值与提醒方式) | P1 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心业务流程
|
||||
|
||||
### 4.1 首次启动与授权
|
||||
|
||||
```
|
||||
启动 App → 合规提示页(F07,用户确认)
|
||||
→ 请求相机权限
|
||||
├─ 授权 → 进入相机主界面
|
||||
└─ 拒绝 → 引导页说明用途,可重新授权
|
||||
```
|
||||
|
||||
### 4.2 实时识别
|
||||
|
||||
```
|
||||
相机主界面(默认后置)
|
||||
→ 画面持续检测(连续 / 标准 / 省电频率)
|
||||
→ 检测到目标:显示彩色检测框 + 类别 + 置信度 + 距离
|
||||
├─ 触发提醒(震动 / 声音,10s 防重复)
|
||||
└─ 目标停留 ≥ 0.5s 保持显示,消失后 2s 内不闪断
|
||||
→ 未检测到目标时:识别疑似生境区域(草丛 / 灌木 / 水面)
|
||||
├─ 黄色虚线框展示,标注"疑似区域"与距离
|
||||
└─ 触发预警(提醒方式与动物检测区分)
|
||||
→ 用户可点击检测框查看识别详情(物种、置信度、时间)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 界面设计
|
||||
|
||||
### 5.1 界面总览
|
||||
|
||||
仅两个界面:**相机主界面**(默认全屏展示)+ **设置页**(右上角齿轮入口)。
|
||||
|
||||
### 5.2 相机主界面(核心界面)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ [低光] [频率] [前/后摄] │ ← 顶部工具
|
||||
│ 相机预览画面(全屏) │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ ┌───────┐ │ │
|
||||
│ │ │ 野鸡 86% │ │ │ ← 检测框(按类别着色)
|
||||
│ │ │ 约 25m │ │ │
|
||||
│ │ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ │ │ ← 疑似生境区域(黄色虚线,预警)
|
||||
│ │ 疑似区域 · 约 20m │ │
|
||||
│ │ └───────┘ │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ [提醒开关] [设置] │ ← 底部操作
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
交互说明:
|
||||
|
||||
- 顶部:检测频率切换(连续 / 标准 / 省电)、低光增强开关(P2)、前后摄像头切换;
|
||||
- 中部:全屏预览,检测框 + 类别 + 置信度实时叠加;
|
||||
- 底部:提醒开关(震动 / 声音)、设置入口;
|
||||
- 支持双指缩放、点击对焦;
|
||||
- 检测到目标时边框高亮 + 震动 / 声音提醒(10s 防重复);
|
||||
- 检测框内显示类别、置信度与大致距离(约 X m),随目标实时更新;
|
||||
- 未检测到目标时,疑似生境区域以黄色虚线框展示(含距离标注)并预警(提醒方式与动物检测区分)。
|
||||
|
||||
### 5.3 识别结果卡片(点击检测框弹出)
|
||||
|
||||
| 元素 | 说明 |
|
||||
| --- | --- |
|
||||
| 物种名称 | 中文名 + 英文标签(如:野鸡 pheasant) |
|
||||
| 置信度 | 百分比进度条 |
|
||||
| 时间 | 检测时刻 |
|
||||
| 距离 | 约 25m(单目估算,误差 ±30%) |
|
||||
|
||||
仅展示信息,无保存操作。
|
||||
|
||||
疑似生境区域(黄色虚线框)点击后展示:区域类别、置信度、距离(约 X m,按参考植被高度估算,误差较大)。
|
||||
|
||||
### 5.4 设置页
|
||||
|
||||
| 分组 | 设置项 | 默认值 |
|
||||
| --- | --- | --- |
|
||||
| 识别 | 置信度阈值 | 0.40 |
|
||||
| 识别 | 检测频率 | 连续 |
|
||||
| 识别 | 低光增强 | 关(P2) |
|
||||
| 识别 | 距离标注 | 开 |
|
||||
| 识别 | 生境区域阈值 | 0.35 |
|
||||
| 提醒 | 震动提醒 / 提示音 / 防重复时长 | 开 / 开 / 10s |
|
||||
| 提醒 | 生境区域预警 | 开 |
|
||||
| 关于 | 版本信息 / 隐私说明 | — |
|
||||
|
||||
---
|
||||
|
||||
## 6. 权限设计
|
||||
|
||||
| 权限 | 用途 | 时机 | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| CAMERA | 实时预览与识别 | 首次启动 | 必须 |
|
||||
|
||||
原则:**最小权限、按需申请**。仅申请相机权限;拒绝后提供说明引导页(可跳转系统设置重新授权)。不申请定位、存储等任何其他权限。
|
||||
|
||||
---
|
||||
|
||||
## 7. 非功能需求
|
||||
|
||||
| 类别 | 要求 |
|
||||
| --- | --- |
|
||||
| 兼容性 | Android 7.0+(minSdk 24),竖屏为主,支持暗色模式 |
|
||||
| 性能 | 见《技术方案》第 7 章(延迟 ≤ 80ms、启动 ≤ 1.5s、内存 ≤ 200MB) |
|
||||
| 离线 | 飞行模式下实时识别全流程完整可用 |
|
||||
| 稳定性 | 崩溃率 ≤ 0.5%,启动成功率 ≥ 99%,异常自动降级(GPU 回退 CPU) |
|
||||
| 耗电 | 连续使用 1 小时 ≤ 15%;省电模式 ≤ 8% |
|
||||
| 隐私 | 无广告 SDK、无埋点、不保存任何数据,识别数据仅存内存 |
|
||||
| 无障碍 | 关键操作支持 TalkBack 描述;检测结果支持语音播报(V2) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 版本规划
|
||||
|
||||
| 版本 | 范围 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| MVP / V1.0 | F01~F05、F07、F08 | 相机 + 实时识别 + 生境预警 + 提醒 + 设置,即完整核心体验 |
|
||||
| V2.0 | +F06 及增强 | 低光增强、更多物种、夜视模式、模型远程更新、语音播报、距离精度增强(地面平面法) |
|
||||
|
||||
---
|
||||
|
||||
## 9. 合规与安全说明
|
||||
|
||||
- App 定位为**野生动物观察、识别工具**,不提供猎捕、诱捕、伤害野生动物的功能或指导;
|
||||
- 首次启动展示合规提示:遵守《中华人民共和国野生动物保护法》,野鸡属"三有"保护动物,猎捕须依法许可;观察时保持距离、不惊扰动物、遵守保护区规定;
|
||||
- 上架需提供隐私政策,声明仅使用相机权限、不采集与存储任何用户数据。
|
||||
@@ -1,512 +0,0 @@
|
||||
# Observer(野视)· 技术实现文档
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档版本 | v1.1 |
|
||||
| 编写日期 | 2026-08-17 |
|
||||
| 状态 | 初稿(v1.1:确认功能范围为"仅实时识别",移除拍照、记录、相册识别、定位相关实现) |
|
||||
| 配套文档 | 《01-技术方案》《02-项目功能文档》 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目结构
|
||||
|
||||
```
|
||||
app/
|
||||
├── src/main/
|
||||
│ ├── java/com/example/observer/
|
||||
│ │ ├── MainActivity.kt // 单 Activity 入口
|
||||
│ │ ├── camera/
|
||||
│ │ │ ├── CameraController.kt // CameraX 生命周期绑定与用例组合
|
||||
│ │ │ └── FrameAnalyzer.kt // ImageAnalysis 帧分析器(节流 + 调度)
|
||||
│ │ ├── detection/
|
||||
│ │ │ ├── Detector.kt // 检测器抽象接口
|
||||
│ │ │ ├── TFLiteDetector.kt // TFLite 实现(YOLOv8n)
|
||||
│ │ │ ├── DetectionResult.kt // 检测结果数据类
|
||||
│ │ │ ├── Nms.kt // NMS 后处理
|
||||
│ │ │ └── CoordinateMapper.kt // 模型坐标 → 视图坐标映射
|
||||
│ │ ├── distance/
|
||||
│ │ │ └── DistanceEstimator.kt // 单目距离估计(针孔模型)
|
||||
│ │ ├── overlay/
|
||||
│ │ │ └── DetectionOverlay.kt // Compose 叠加层(Canvas 绘制检测框)
|
||||
│ │ ├── reminder/
|
||||
│ │ │ └── Reminder.kt // 震动 / 提示音提醒(含防重复)
|
||||
│ │ ├── ui/
|
||||
│ │ │ ├── camera/ CameraScreen.kt / CameraViewModel.kt
|
||||
│ │ │ └── settings/ SettingsScreen.kt / SettingsViewModel.kt
|
||||
│ │ └── util/
|
||||
│ │ └── BitmapUtils.kt // 缩放 / 旋转 / 复用
|
||||
│ ├── assets/
|
||||
│ │ ├── model.tflite // YOLOv8n 四类模型
|
||||
│ │ └── labels.txt // 类别标签(按 ID 顺序)
|
||||
│ └── res/
|
||||
└── build.gradle.kts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈与版本
|
||||
|
||||
| 组件 | 版本(以最新稳定为准) | 用途 |
|
||||
| --- | --- | --- |
|
||||
| Kotlin | 2.x | 开发语言 |
|
||||
| AGP | 8.x | Android Gradle 插件 |
|
||||
| Jetpack Compose | BOM 2024.x+(Material 3) | UI |
|
||||
| CameraX | 1.4.x | Preview / ImageAnalysis |
|
||||
| TensorFlow Lite | 2.16.x | 端侧推理 |
|
||||
| tensorflow-lite-gpu | 2.16.x | GPU 加速 |
|
||||
| DataStore | 1.1.x | 设置存储 |
|
||||
|
||||
依赖(build.gradle.kts 关键片段):
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("androidx.camera:camera-core:1.4.1")
|
||||
implementation("androidx.camera:camera-camera2:1.4.1")
|
||||
implementation("androidx.camera:camera-lifecycle:1.4.1")
|
||||
implementation("androidx.camera:camera-view:1.4.1")
|
||||
|
||||
implementation("org.tensorflow:tensorflow-lite:2.16.1")
|
||||
implementation("org.tensorflow:tensorflow-lite-gpu:2.16.1")
|
||||
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 模块设计
|
||||
|
||||
### 3.1 camera 模块 — CameraController.kt
|
||||
|
||||
职责:创建并绑定 CameraX 用例(Preview + ImageAnalysis),统一生命周期。
|
||||
|
||||
```kotlin
|
||||
class CameraController(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val analysisExecutor: Executor,
|
||||
) {
|
||||
private lateinit var cameraProvider: ProcessCameraProvider
|
||||
private lateinit var imageAnalysis: ImageAnalysis
|
||||
|
||||
fun start(previewView: PreviewView, analyzer: ImageAnalysis.Analyzer) {
|
||||
val future = ProcessCameraProvider.getInstance(previewView.context)
|
||||
future.addListener({
|
||||
cameraProvider = future.get()
|
||||
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.surfaceProvider = previewView.surfaceProvider
|
||||
}
|
||||
|
||||
imageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
|
||||
.build()
|
||||
imageAnalysis.setAnalyzer(analysisExecutor, analyzer)
|
||||
|
||||
cameraProvider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview, imageAnalysis,
|
||||
)
|
||||
}, ContextCompat.getMainExecutor(previewView.context))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- `OUTPUT_IMAGE_FORMAT_RGBA_8888`(CameraX 1.3+)直接得到 RGBA 图像,免 YUV 转换;
|
||||
- `STRATEGY_KEEP_ONLY_LATEST`:分析器忙时丢弃旧帧,不积压;
|
||||
- 相机权限检查与请求在进入本模块前完成;
|
||||
- 前后摄切换:重新以对应 `CameraSelector` 执行 `bindToLifecycle`(解绑旧用例)。
|
||||
|
||||
### 3.2 camera 模块 — FrameAnalyzer.kt
|
||||
|
||||
职责:帧节流、旋转获取、调度检测、结果回调。
|
||||
|
||||
```kotlin
|
||||
class FrameAnalyzer(
|
||||
private val detector: Detector,
|
||||
private val onResult: (List<DetectionResult>, Int) -> Unit, // 结果 + 旋转角
|
||||
) : ImageAnalysis.Analyzer {
|
||||
|
||||
private var lastDetectMs = 0L
|
||||
|
||||
override fun analyze(imageProxy: ImageProxy) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val interval = frameIntervalMs() // 依据设置:连续/标准/省电
|
||||
if (now - lastDetectMs < interval) {
|
||||
imageProxy.close(); return
|
||||
}
|
||||
lastDetectMs = now
|
||||
|
||||
val bitmap = imageProxy.toBitmap() // RGBA_8888 直接转 Bitmap
|
||||
val rotation = imageProxy.imageInfo.rotationDegrees
|
||||
try {
|
||||
val results = detector.detect(bitmap)
|
||||
onResult(results, rotation)
|
||||
} finally {
|
||||
imageProxy.close() // 必须关闭,否则阻塞流
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 detection 模块 — Detector 接口
|
||||
|
||||
```kotlin
|
||||
interface Detector {
|
||||
/** 输入 RGBA 位图,输出归一化坐标检测结果(0..1) */
|
||||
fun detect(bitmap: Bitmap): List<DetectionResult>
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 detection 模块 — TFLiteDetector.kt
|
||||
|
||||
```kotlin
|
||||
class TFLiteDetector(
|
||||
context: Context,
|
||||
private val inputSize: Int = 320,
|
||||
private val confThreshold: Float = 0.40f, // 动物类阈值
|
||||
private val habitatThreshold: Float = 0.35f, // 生境区域阈值
|
||||
private val iouThreshold: Float = 0.45f,
|
||||
gpuEnabled: Boolean = true,
|
||||
) : Detector {
|
||||
|
||||
private val labels: List<String> =
|
||||
context.assets.open("labels.txt").bufferedReader().readLines()
|
||||
|
||||
private val interpreter: Interpreter = Interpreter(
|
||||
loadModelFile(context, "model.tflite"),
|
||||
Interpreter.Options().apply {
|
||||
setNumThreads(4)
|
||||
if (gpuEnabled) {
|
||||
try { addDelegate(GpuDelegate()) } catch (_: Exception) { /* 回退 CPU */ }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val inputBuffer: ByteBuffer = ByteBuffer.allocateDirect(
|
||||
1 * inputSize * inputSize * 3 * 4 // float32
|
||||
).order(ByteOrder.nativeOrder())
|
||||
|
||||
private val outputBuffer: ByteBuffer = ByteBuffer.allocateDirect(
|
||||
OUTPUT_ELEMENTS * 4
|
||||
).order(ByteOrder.nativeOrder())
|
||||
|
||||
override fun detect(bitmap: Bitmap): List<DetectionResult> {
|
||||
preprocess(bitmap, inputBuffer) // 缩放 + RGB 归一化 → 输入缓冲
|
||||
interpreter.run(inputBuffer, outputBuffer) // 单张推理
|
||||
return postprocess(outputBuffer, bitmap.width, bitmap.height)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输入 / 输出规格(YOLOv8n 四类,320 输入):
|
||||
|
||||
- 输入:`[1, 320, 320, 3]`,RGB,float32 归一化 0~1;
|
||||
- 输出:`[1, 9, 8400]`(8400 = 各尺度 anchor 数,9 = 4 个框坐标 cx/cy/w/h + 5 类得分),展平为 `9 * 8400` 个 float。
|
||||
|
||||
### 3.5 detection 模块 — 后处理(解码 + NMS)
|
||||
|
||||
```kotlin
|
||||
private fun postprocess(raw: FloatArray, imgW: Int, imgH: Int): List<DetectionResult> {
|
||||
val numAnchor = raw.size / 9
|
||||
val boxes = mutableListOf<DetectionResult>()
|
||||
|
||||
for (a in 0 until numAnchor) {
|
||||
val cx = raw[a]; val cy = raw[9 + a]
|
||||
val w = raw[18 + a]; val h = raw[27 + a]
|
||||
var bestCls = 0; var bestScore = 0f
|
||||
for (c in 0 until 5) {
|
||||
val s = raw[36 + c * numAnchor + a] // 类别得分按 anchor 平铺
|
||||
if (s > bestScore) { bestScore = s; bestCls = c }
|
||||
}
|
||||
if (bestScore < confThreshold) continue
|
||||
boxes += DetectionResult(
|
||||
label = labels[bestCls],
|
||||
score = bestScore,
|
||||
// 归一化坐标(裁剪到 [0,1])
|
||||
left = (cx - w / 2).coerceIn(0f, 1f),
|
||||
top = (cy - h / 2).coerceIn(0f, 1f),
|
||||
right = (cx + w / 2).coerceIn(0f, 1f),
|
||||
bottom = (cy + h / 2).coerceIn(0f, 1f),
|
||||
)
|
||||
}
|
||||
return nms(boxes, iouThreshold)
|
||||
}
|
||||
|
||||
fun nms(boxes: List<DetectionResult>, iouThreshold: Float): List<DetectionResult> {
|
||||
val sorted = boxes.sortedByDescending { it.score }
|
||||
val kept = mutableListOf<DetectionResult>()
|
||||
for (b in sorted) {
|
||||
if (kept.none { iou(b, it) > iouThreshold }) kept += b
|
||||
}
|
||||
return kept
|
||||
}
|
||||
```
|
||||
|
||||
注意:以上取数索引为示意,**必须与模型导出时的输出布局(yolov8 tflite 为 9×8400,按列平铺)核对一致**,建议训练导出后用 Python 脚本先对单图做一致性校验再接入 App。
|
||||
|
||||
### 3.6 distance 模块 — DistanceEstimator.kt
|
||||
|
||||
```kotlin
|
||||
class DistanceEstimator(context: Context) {
|
||||
|
||||
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
private var focalPxCache = -1f
|
||||
|
||||
// 物种参考体型(米),用于针孔模型估算
|
||||
private val speciesSizeM = mapOf(
|
||||
"pheasant" to 0.45f, // 身高
|
||||
)
|
||||
|
||||
/** 距离 = 焦距px × 参考体型 / 框高px(在分析图像分辨率下计算) */
|
||||
fun estimate(label: String, boxHeightNorm: Float, imageHeightPx: Int): Float? {
|
||||
val realH = speciesSizeM[label] ?: return null
|
||||
val boxH = boxHeightNorm * imageHeightPx
|
||||
if (boxH < 8f) return null // 过小目标不估算
|
||||
val focalPx = focalPx(imageHeightPx)
|
||||
if (focalPx <= 0) return null
|
||||
return round(focalPx * realH / boxH)
|
||||
}
|
||||
|
||||
/** focal_px = focal_mm × (imageHeightPx / sensorHeightMm);视场角作回退 */
|
||||
private fun focalPx(imageHeightPx: Int): Float {
|
||||
if (focalPxCache > 0) return focalPxCache
|
||||
val c = cameraManager.getCameraCharacteristics(cameraManager.cameraIdList.first())
|
||||
val focalMm = c.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)?.firstOrNull()
|
||||
val sensor = c.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
|
||||
val fov = c.get(CameraCharacteristics.LENS_INFO_HORIZONTAL_VIEW_ANGLE)
|
||||
focalPxCache = when {
|
||||
focalMm != null && sensor != null -> focalMm * imageHeightPx / sensor.height
|
||||
fov != null -> (imageHeightPx / 2f) / tan(fov / 2f)
|
||||
else -> -1f
|
||||
}
|
||||
return focalPxCache
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 前后摄切换时需按当前相机重新获取焦距(缓存按相机 ID 区分);
|
||||
- 距离估算仅供参考,实际距离可能因环境因素有所偏差。
|
||||
|
||||
### 3.7 detection 模块 — CoordinateMapper.kt
|
||||
|
||||
模型输出为归一化坐标(相对分析图像,竖屏方向),需转换到预览视图坐标:
|
||||
|
||||
```kotlin
|
||||
class CoordinateMapper(private val view: PreviewView) {
|
||||
|
||||
/** 归一化坐标 → 预览视图像素坐标(考虑传感器旋转与 FIT_CENTER 裁剪) */
|
||||
fun mapToView(norm: DetectionResult, rotation: Int): RectF {
|
||||
// 1) 旋转校正:把"图像方向"归一化坐标转到"竖屏视图方向"
|
||||
val (x0, y0, x1, y1) = rotate(norm, rotation)
|
||||
// 2) 处理 FIT_CENTER 的裁剪偏移与缩放
|
||||
val viewW = view.width; val viewH = view.height
|
||||
val scale = min(viewW / bitmapW, viewH / bitmapH) // bitmap 尺寸
|
||||
val offsetX = (viewW - bitmapW * scale) / 2f
|
||||
val offsetY = (viewH - bitmapH * scale) / 2f
|
||||
return RectF(
|
||||
x0 * bitmapW * scale + offsetX,
|
||||
y0 * bitmapH * scale + offsetY,
|
||||
x1 * bitmapW * scale + offsetX,
|
||||
y1 * bitmapH * scale + offsetY,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.8 overlay 模块 — DetectionOverlay.kt
|
||||
|
||||
Compose Canvas 叠加层:
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun DetectionOverlay(results: List<DetectionResult>, rotation: Int, modifier: Modifier) {
|
||||
val mapper = remember { CoordinateMapper(view) }
|
||||
Canvas(modifier = modifier) {
|
||||
results.forEach { r ->
|
||||
val rect = mapper.mapToView(r, rotation)
|
||||
drawRect(
|
||||
color = colorOf(r.label),
|
||||
topLeft = Offset(rect.left, rect.top),
|
||||
size = Size(rect.width(), rect.height()),
|
||||
style = Stroke(6.dp.toPx()),
|
||||
)
|
||||
// 距离由 ViewModel 计算后写入 DetectionResult.distance(Float?)
|
||||
val dist = r.distance?.let { " · 约${it}m" } ?: ""
|
||||
drawText("${r.label} ${(r.score * 100).toInt()}%$dist")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
防闪烁:ViewModel 中对结果做"目标停留 ≥ 0.5s 才显示、消失 2s 后移除"的平滑处理。
|
||||
|
||||
### 3.9 reminder 模块 — Reminder.kt
|
||||
|
||||
```kotlin
|
||||
class Reminder(private val context: Context) {
|
||||
private val vibrator = context.getSystemService(Vibrator::class.java)
|
||||
private var lastAlertAt = 0L
|
||||
private var lastAlertLabel: String? = null
|
||||
|
||||
/** 同类目标 10s 内只提醒一次 */
|
||||
fun onDetected(label: String) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (label == lastAlertLabel && now - lastAlertAt < 10_000) return
|
||||
lastAlertAt = now
|
||||
lastAlertLabel = label
|
||||
vibrator?.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.10 设置存储 — DataStore
|
||||
|
||||
使用 DataStore Preferences 保存:`conf_threshold`(默认 0.40)、`detect_mode`(连续/标准/省电)、`vibrate_enabled`、`sound_enabled`、`low_light_enhance`(P2)、`show_distance`(默认开)。`SettingsViewModel` 以 Flow 暴露,`FrameAnalyzer` 与 `TFLiteDetector` 读取最新值。
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键流程实现
|
||||
|
||||
### 4.1 启动流程
|
||||
|
||||
```
|
||||
MainActivity.onCreate
|
||||
→ 检查/请求 CAMERA 权限
|
||||
→ 初始化 TFLiteDetector(后台线程,含预热推理)
|
||||
→ CameraController.start(previewView, analyzer)
|
||||
→ ViewModel 订阅检测结果 → 叠加层渲染 + 提醒触发
|
||||
```
|
||||
|
||||
预热:加载模型后执行一次空推理(全零输入),避免首帧卡顿。
|
||||
|
||||
### 4.2 帧节流与并发控制
|
||||
|
||||
- 检测间隔:连续 80~100ms / 标准 300ms / 省电 1000ms(由设置决定);
|
||||
- 推理在单线程 Executor(HandlerThread)串行执行,天然互斥;
|
||||
- 分析线程只做"取帧 → 判断节流 → 提交任务",不做推理,保证预览流畅。
|
||||
|
||||
### 4.3 提醒、距离标注与防闪烁
|
||||
|
||||
- 检测到目标(置信度 ≥ 阈值,生境区域用独立阈值)→ `Reminder.onDetected(label)`,10s 防重复,动物与生境预警提醒方式区分;
|
||||
- 叠加层展示逻辑:目标连续出现 ≥ 0.5s 才绘制;消失后 2s 内不清除,避免闪烁;
|
||||
- 低置信度结果(阈值以下)不展示、不提醒;
|
||||
- 距离标注:结果到达后调用 `DistanceEstimator` 估算"约 X m",随检测框渲染(FrameAnalyzer 回调携带分析图像高度);生境区域同样估算(按参考植被高度,误差较大);框高过小(< 8px)或无法获取焦距显示"--"。
|
||||
|
||||
---
|
||||
|
||||
## 5. 模型集成细节
|
||||
|
||||
| 项 | 说明 |
|
||||
| --- | --- |
|
||||
| 模型文件 | `app/src/main/assets/model.tflite`,随 APK 打包 |
|
||||
| 标签文件 | `assets/labels.txt`,每行一个类别,顺序与训练一致(pheasant) |
|
||||
| 量化 | 优先 fp16(GPU);int8 用于 CPU/低端机回退 |
|
||||
| 加载 | `Interpreter(loadAssetFile(...))`,进程内单例 |
|
||||
| 预热 | 初始化后执行一次 dummy run |
|
||||
| 校验 | 发布前用 Python 脚本(tflite-runtime)对测试集抽样验证模型输出布局与 App 解析一致 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能优化实践
|
||||
|
||||
| 手段 | 说明 |
|
||||
| --- | --- |
|
||||
| GPU Delegate | 优先启用;`addDelegate` 抛异常或首帧异常时回退 CPU,并上报降级日志 |
|
||||
| 输入缓冲复用 | ByteBuffer 复用,避免每次检测重新分配 |
|
||||
| 帧节流 | 按模式降频,KEEP_ONLY_LATEST 不积压 |
|
||||
| 位图复用 | ImageProxy → Bitmap 走共享缓冲;叠加层仅绘制检测框,不复制整帧 |
|
||||
| 线程模型 | 检测线程单线程串行;UI 线程零推理 |
|
||||
| 低分辨率检测 | 320 输入检测;远距离模式可切 416(V2) |
|
||||
| 距离标注 | 纯算术运算开销可忽略;焦距 px 按相机缓存复用,避免重复查询 CameraCharacteristics |
|
||||
| 生命周期 | 后台自动 `unbind` 相机释放资源,避免耗电 |
|
||||
| 省电模式 | 降到 1 帧/秒 + 低分辨率 + 关闭提醒以外的动画 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 异常与降级策略
|
||||
|
||||
| 异常场景 | 处理 |
|
||||
| --- | --- |
|
||||
| GPU 不可用 / 模型加载失败 | 回退 CPU 线程数调整;失败则提示"识别不可用"但不影响相机预览 |
|
||||
| 相机初始化失败(个别机型) | 重试一次 → 失败提示并引导检查权限/重启 |
|
||||
| 权限被拒 | 引导页说明用途,提供跳转系统设置 |
|
||||
| 低内存 | 主动释放缓存位图,降低检测分辨率 |
|
||||
| 推理超时(> 500ms) | 丢弃该帧结果,恢复下一帧,保证预览流畅 |
|
||||
| 无法读取焦距 / 视场角(个别机型) | 距离显示"--",识别不受影响 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试方案
|
||||
|
||||
### 8.1 单元测试(JUnit)
|
||||
|
||||
- `NmsTest`:NMS 正确性(重叠/多类别/边界框);
|
||||
- `CoordinateMapperTest`:四向旋转映射、FIT_CENTER 裁剪偏移;
|
||||
- `ThresholdTest`:置信度阈值过滤逻辑;
|
||||
- `ReminderTest`:10s 防重复提醒逻辑、动物与生境提醒方式区分;
|
||||
- `DistanceEstimatorTest`:焦距换算、距离公式、过小目标不估算、生境区域按植被高度估算。
|
||||
|
||||
### 8.2 模型评估(Python 脚本,独立于 App)
|
||||
|
||||
- 对测试集计算 mAP@0.5、mAP@0.5:0.95、各类别 AP、负样本误检率;
|
||||
- 输出混淆矩阵,分析检测精度。
|
||||
|
||||
### 8.3 仪器测试(androidTest)
|
||||
|
||||
- 模拟帧注入:将测试图片经 `ImageProxy` 注入分析器,断言结果(不依赖真机取景);
|
||||
- 权限拒绝 / 恢复场景;
|
||||
- 冷启动到预览的时间基准测试。
|
||||
|
||||
### 8.4 真机测试矩阵
|
||||
|
||||
| 机型档位 | 代表机型 | 验证项 |
|
||||
| --- | --- | --- |
|
||||
| 旗舰 | 骁龙 8 系 / 麒麟 9 系 | 全功能、GPU 路径、长时间发热 |
|
||||
| 中端 | 骁龙 7 系 / 天玑 8 系 | 延迟 ≤ 80ms、耗电 ≤ 15%/h |
|
||||
| 低端 | 骁龙 4 系 / 天玑 6 系 | CPU 回退路径、省电模式可用性 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 构建与发布
|
||||
|
||||
```kotlin
|
||||
android {
|
||||
compileSdk = 35
|
||||
defaultConfig {
|
||||
applicationId = "com.example.observer"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
ndk { abiFilters += listOf("arm64-v8a") } // 主发 arm64;如需兼容 32 位另发包
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- R8 规则:保留 TFLite 相关类(`-keep class org.tensorflow.** { *; }`),assets 模型不混淆;
|
||||
- 签名:正式签名 + Gradle 管理(或环境变量注入),不上传 keystore 到仓库;
|
||||
- 上架检查:隐私政策(仅相机权限、不采集与存储数据声明)、权限用途说明。
|
||||
|
||||
---
|
||||
|
||||
## 10. 后续演进(V2 方向)
|
||||
|
||||
- 更多物种类别(哺乳类、鸟类细分),支持模型远程更新;
|
||||
- 小目标优化:SAHI 切图推理 / 专用小目标模型 / 数字变焦辅助;
|
||||
- 夜视 / 红外增强模式;
|
||||
- 检测结果语音播报,提升无障碍体验;
|
||||
- 距离精度增强:基于设备俯仰角与相机高度的地面平面法、镜头畸变校正、水下折射修正;
|
||||
- 生境类别细分与精度提升(湿地、林缘等),生境预警策略优化;
|
||||
- 若未来需要留存能力(拍照、记录),可基于现有检测链路平滑扩展。
|
||||
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "058e0af2c2b57e369d905a03ac9748b0ebf543c6"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
|
||||
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
|
||||
- platform: ios
|
||||
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
|
||||
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,33 @@
|
||||
# observer
|
||||
|
||||
野生动物实时识别 App(Flutter 版)。Android / iOS 一套代码,后端接口与支付见
|
||||
[`docs/PaymentApi.md`](docs/PaymentApi.md)。
|
||||
|
||||
## iOS 真机部署(iPhone)
|
||||
|
||||
### 构建与安装
|
||||
|
||||
```bash
|
||||
# 真机必须传 Mac 局域网 IP:默认 API_BASE_URL 是 10.0.2.2(仅 Android 模拟器可用),
|
||||
# 不传则 iPhone 上所有网络请求(登录/授权/套餐)都会失败
|
||||
flutter build ios --release --dart-define=API_BASE_URL=http://<Mac局域网IP>:8080
|
||||
|
||||
# 安装到真机(UDID 可用 `xcrun devicectl list devices` 查询)
|
||||
xcrun devicectl device install app --device <UDID> build/ios/iphoneos/Runner.app
|
||||
|
||||
# 启动并抓控制台日志(--terminate-existing 先杀掉旧实例)
|
||||
xcrun devicectl device process launch --console --terminate-existing \
|
||||
--device <UDID> com.observer.app
|
||||
```
|
||||
|
||||
### 注意事项(踩过的坑)
|
||||
|
||||
- **debug 构建不能在真机上从桌面图标启动**:iOS 14+ 会提示
|
||||
"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling"。
|
||||
debug 调试必须用 `flutter run -d <设备ID>` 或 Xcode IDE 启动(`flutter devices` 查设备ID);
|
||||
从图标启动只对 release 构建有效。
|
||||
- **模型输入是 NHWC**:`assets/model.tflite` 做过字节级手术(开头 TRANSPOSE→RESHAPE,
|
||||
输入 [1,320,320,3]),改动记录见 git 历史,重导模型需同步处理,否则 iOS 报
|
||||
"Node number 0 (TRANSPOSE) failed to prepare"。
|
||||
- **模拟器黑屏**:本机 iOS 模拟器 Impeller 渲染黑屏,验证 UI 用 VM service
|
||||
(`flutter run` 输出里的 DevTools 地址),或直接真机验证。
|
||||
@@ -0,0 +1,28 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,53 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.observer"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.observer"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
// tflite_flutter 依赖的 tensorflow-lite / tensorflow-lite-gpu / tensorflow-lite-api 三个 AAR
|
||||
// 声明了相同 namespace(org.tensorflow.lite),新 AGP 视作冲突直接报错;
|
||||
// 本项目仅用 CPU 推理,GPU delegate 未使用,排除 gpu 及其传递依赖的 api 即可。
|
||||
configurations.all {
|
||||
exclude(group = "org.tensorflow", module = "tensorflow-lite-gpu")
|
||||
exclude(group = "org.tensorflow", module = "tensorflow-lite-api")
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,48 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
android:label="视野"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.observer
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# This newDsl flag was added by the Flutter template
|
||||
android.newDsl=false
|
||||
# This builtInKotlin flag was added by the Flutter template
|
||||
android.builtInKotlin=false
|
||||
@@ -1,9 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "9.0.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
@@ -1,2 +1,2 @@
|
||||
pheasant
|
||||
cover
|
||||
suspect
|
||||
@@ -0,0 +1,103 @@
|
||||
# 后端 API 契约(账号 + 支付/授权)
|
||||
|
||||
客户端(Flutter)与后端之间的 REST 契约。账号体系:手机号 + 密码注册登录,登录返回自签名 token,后续接口携带 `Authorization: Bearer <token>`;**识别入口(搜索按钮)必须强制服务端校验授权**,不走本地缓存。
|
||||
|
||||
- Base URL: `AppConfig.apiBaseUrl`(占位 `https://YOUR_BACKEND.example.com`)
|
||||
- 响应统一格式: `{"code": 0, "message": "ok", "data": {...}}`,`code != 0` 视为失败;登录失效返回 `code 61`,客户端应回登录页
|
||||
- 授权语义: 自然日(当天 24:00 失效 / 7 天 / 30 天),以服务端为准
|
||||
- 账号标识: 手机号(服务端 license 表即账号表,手机号为主键;无独立用户表)
|
||||
|
||||
## 1. 注册
|
||||
|
||||
`POST /api/v1/auth/register`(公开,无需登录)
|
||||
|
||||
```json
|
||||
{"phone": "13800138000", "password": "pass123456"}
|
||||
```
|
||||
|
||||
响应 `data` 为空对象。重复注册报错;已被运营手动授权(发卡占位行)的手机号可注册补密码,授权保留。
|
||||
|
||||
## 2. 登录
|
||||
|
||||
`POST /api/v1/auth/login`(公开,无需登录)
|
||||
|
||||
```json
|
||||
{"phone": "13800138000", "password": "pass123456"}
|
||||
```
|
||||
|
||||
响应 `data`:
|
||||
|
||||
```json
|
||||
{"token": "<HMAC-SHA256 自签名 token>"}
|
||||
```
|
||||
|
||||
- token 无状态,有效期 `auth.tokenTtl`(默认 30 天),客户端存 secure storage
|
||||
- 后续所有接口携带 `Authorization: Bearer <token>`
|
||||
|
||||
## 3. 创建订单
|
||||
|
||||
`POST /api/v1/orders`(需登录)
|
||||
|
||||
```json
|
||||
{"planId": "day|week|month", "channel": "wechat|alipay"}
|
||||
```
|
||||
|
||||
响应 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"orderId": "O20260822001",
|
||||
"payParams": {
|
||||
"partnerId": "1900xxxxx", "prepayId": "wx...", "nonceStr": "...",
|
||||
"timeStamp": "1728000000", "sign": "...", "packageValue": "Sign=WXPay"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 手机号由 token 识别,请求体不含 deviceId
|
||||
- `channel == wechat` 时 `payParams` 为微信 APP 支付下单参数
|
||||
- `channel == alipay` 时 `payParams` 为 `{"orderStr": "alipay_sdk=..."}`
|
||||
|
||||
## 4. 支付结果确认
|
||||
|
||||
`POST /api/v1/orders/{orderId}/confirm`(需登录)
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
客户端拉起 SDK 支付成功后调用(幂等)。服务端以微信/支付宝异步回调为准落授权;confirm 仅用于加速刷新。响应 `data: {"status": "paid|created|closed"}`。
|
||||
|
||||
## 5. 查询授权
|
||||
|
||||
`GET /api/v1/license`(需登录)
|
||||
|
||||
响应 `data`:
|
||||
|
||||
```json
|
||||
{"active": true, "expiresAt": "2026-08-29T23:59:59+08:00"}
|
||||
```
|
||||
|
||||
- `active: false` 或 `expiresAt` 已过期 → 客户端展示付费墙/充值入口
|
||||
- **识别入口(搜索按钮)必须调本接口做强制校验**:网络失败视为不可用并提示,禁止用本地缓存放行
|
||||
- 主界面到期时间展示可用本地缓存(服务端为准,刷新时覆盖)
|
||||
|
||||
## 6. 套餐
|
||||
|
||||
`GET /api/v1/plans`(需登录)拉取价格方案,**客户端不硬编码价格**(进充值页时拉取,价格以服务端为准)。
|
||||
|
||||
响应 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{"planId": "day", "days": 1, "priceCents": 1000},
|
||||
{"planId": "week", "days": 7, "priceCents": 5600},
|
||||
{"planId": "month", "days": 30, "priceCents": 18000}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `priceCents` 为整数分,客户端展示 ÷100 转元
|
||||
- 展示名由客户端按 `days` 派生「N天」,接口无 label 字段
|
||||
- 后端套餐来自 `config.yml` `plans` 节点(静态定价,改价改配置重启生效)
|
||||
@@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,66 @@
|
||||
platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
|
||||
# 微信 SDK podspec 对模拟器保守排除 arm64(其 xcframework 实际含 arm64 slice),
|
||||
# 不清除会导致 M 系 Mac 上模拟器构建被压成 x86_64 而无法安装运行。
|
||||
wechat = installer.pods_project.targets.find { |t| t.name == 'WechatOpenSDK-XCFramework' }
|
||||
wechat&.build_configurations&.each do |config|
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = ''
|
||||
end
|
||||
|
||||
# WechatOpenSDK-XCFramework 的 Headers 不会自动进入依赖方搜索路径
|
||||
# (CocoaPods 对 vendored XCFramework 的已知行为),fluwx 以引号引入
|
||||
# WXApi.h 需要显式补充头文件搜索路径。
|
||||
wechat_headers = Dir.glob(
|
||||
File.join(Pod::Config.instance.project_root, 'Pods', 'WechatOpenSDK-XCFramework',
|
||||
'WechatOpenSDK.xcframework', '*', 'WechatOpenSDK.framework', 'Headers')
|
||||
)
|
||||
unless wechat_headers.empty?
|
||||
fluwx = installer.pods_project.targets.find { |t| t.name == 'fluwx' }
|
||||
fluwx&.build_configurations&.each do |config|
|
||||
config.build_settings['HEADER_SEARCH_PATHS'] = [
|
||||
'$(inherited)',
|
||||
*wechat_headers.map { |p| "\"#{p}\"" },
|
||||
].join(' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- fluwx (0.0.1):
|
||||
- Flutter
|
||||
- fluwx/pay (= 0.0.1)
|
||||
- fluwx/pay (0.0.1):
|
||||
- Flutter
|
||||
- WechatOpenSDK-XCFramework (~> 2.0.4)
|
||||
- TensorFlowLiteC (2.12.0):
|
||||
- TensorFlowLiteC/Core (= 2.12.0)
|
||||
- TensorFlowLiteC/Core (2.12.0)
|
||||
- TensorFlowLiteC/CoreML (2.12.0):
|
||||
- TensorFlowLiteC/Core
|
||||
- TensorFlowLiteC/Metal (2.12.0):
|
||||
- TensorFlowLiteC/Core
|
||||
- TensorFlowLiteSwift (2.12.0):
|
||||
- TensorFlowLiteSwift/Core (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Core (2.12.0):
|
||||
- TensorFlowLiteC (= 2.12.0)
|
||||
- TensorFlowLiteSwift/CoreML (2.12.0):
|
||||
- TensorFlowLiteC/CoreML (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Core (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Metal (2.12.0):
|
||||
- TensorFlowLiteC/Metal (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Core (= 2.12.0)
|
||||
- tflite_flutter (0.0.1):
|
||||
- Flutter
|
||||
- TensorFlowLiteSwift (= 2.12.0)
|
||||
- TensorFlowLiteSwift/CoreML (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Metal (= 2.12.0)
|
||||
- tobias (0.0.1):
|
||||
- Flutter
|
||||
- tobias/normal (= 0.0.1)
|
||||
- tobias/normal (0.0.1):
|
||||
- Flutter
|
||||
- vibration (1.7.5):
|
||||
- Flutter
|
||||
- WechatOpenSDK-XCFramework (2.0.7)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- fluwx (from `.symlinks/plugins/fluwx/ios`)
|
||||
- tflite_flutter (from `.symlinks/plugins/tflite_flutter/ios`)
|
||||
- tobias (from `.symlinks/plugins/tobias/ios`)
|
||||
- vibration (from `.symlinks/plugins/vibration/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- TensorFlowLiteC
|
||||
- TensorFlowLiteSwift
|
||||
- WechatOpenSDK-XCFramework
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
fluwx:
|
||||
:path: ".symlinks/plugins/fluwx/ios"
|
||||
tflite_flutter:
|
||||
:path: ".symlinks/plugins/tflite_flutter/ios"
|
||||
tobias:
|
||||
:path: ".symlinks/plugins/tobias/ios"
|
||||
vibration:
|
||||
:path: ".symlinks/plugins/vibration/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
|
||||
TensorFlowLiteC: 20785a69299185a379ba9852b6625f00afd7984a
|
||||
TensorFlowLiteSwift: 3a4928286e9e35bdd3e17970f48e53c80d25e793
|
||||
tflite_flutter: 64b192e11352fe36943ab6656e1d49207f1a5595
|
||||
tobias: 7bc370eaccba2e7c7c345902a4a47dc5916cf8bb
|
||||
vibration: 8e2f50fc35bb736f9eecb7dd9f7047fbb6a6e888
|
||||
WechatOpenSDK-XCFramework: 5df9b250e9839dcc306ad8b00f46822eead1ed47
|
||||
|
||||
PODFILE CHECKSUM: bee538157bfc80e3e10d48a376da3677058aaad6
|
||||
|
||||
COCOAPODS: 1.17.0
|
||||
@@ -0,0 +1,782 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
847056A2B331EBEF7FE4658D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44D6E292382D4D067EB56539 /* Pods_Runner.framework */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
9926B0B1CD9F66E289877411 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 324F3F864FEBF7907AB5D311 /* Pods_RunnerTests.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
11C083119EC1D6C49118DBAB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
324F3F864FEBF7907AB5D311 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
352A4BA150122CC2D593B1FF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
40021A3125FEC9A6F074E182 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
44D6E292382D4D067EB56539 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
542B41CC0975A03C978D0E7D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
6CCFCD2B5C2A6DB29ED6FEE9 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
95A9E03BE9A266B55717C203 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
B89928717BDFD7B081FE98E3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
8A8301CDECC0F90E622D6F74 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9926B0B1CD9F66E289877411 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
847056A2B331EBEF7FE4658D /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
684998EFA363788852AAAD42 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
44D6E292382D4D067EB56539 /* Pods_Runner.framework */,
|
||||
324F3F864FEBF7907AB5D311 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
F8827B4F9A04615E3B2278F0 /* Pods */,
|
||||
684998EFA363788852AAAD42 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
6CCFCD2B5C2A6DB29ED6FEE9 /* Runner.entitlements */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F8827B4F9A04615E3B2278F0 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
352A4BA150122CC2D593B1FF /* Pods-Runner.debug.xcconfig */,
|
||||
11C083119EC1D6C49118DBAB /* Pods-Runner.release.xcconfig */,
|
||||
95A9E03BE9A266B55717C203 /* Pods-Runner.profile.xcconfig */,
|
||||
B89928717BDFD7B081FE98E3 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
542B41CC0975A03C978D0E7D /* Pods-RunnerTests.release.xcconfig */,
|
||||
40021A3125FEC9A6F074E182 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
435F2587C2B98BB6FE35CA42 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
8A8301CDECC0F90E622D6F74 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
1A05B1452EF4ACCDC4D1537B /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
8C9F4CABE92F7C7C5DE4A971 /* [CP] Embed Pods Frameworks */,
|
||||
8E27EE045B33D54A69365374 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
1A05B1452EF4ACCDC4D1537B /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
435F2587C2B98BB6FE35CA42 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
8C9F4CABE92F7C7C5DE4A971 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
8E27EE045B33D54A69365374 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QRN5J857S2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.observer.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B89928717BDFD7B081FE98E3 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.observer.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 542B41CC0975A03C978D0E7D /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.observer.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 40021A3125FEC9A6F074E182 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.observer.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QRN5J857S2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.observer.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QRN5J857S2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.observer.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,16 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24765" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24743"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-248" y="7"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>视野</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>observer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>wx0000000000000000</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>alipay0000000000</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>weixin</string>
|
||||
<string>weixinULAPI</string>
|
||||
<string>weixinURLParamsAPI</string>
|
||||
<string>alipay</string>
|
||||
<string>alipays</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要使用相机进行野生动物实时识别</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>需要通过本地网络连接服务器进行账号验证和支付</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||