Compare commits
2 Commits
84d6a8fea9
...
44fe4d963c
| Author | SHA1 | Date | |
|---|---|---|---|
| 44fe4d963c | |||
| 0fe9ed4998 |
73
.gitignore
vendored
73
.gitignore
vendored
@@ -1,71 +1,6 @@
|
|||||||
# Built application files
|
# Ignore Windows reserved files
|
||||||
*.apk
|
NUL
|
||||||
*.ap_
|
|
||||||
*.aab
|
|
||||||
|
|
||||||
# Files for the ART/Dalvik VM
|
# Android/ Gradle build outputs (optional for CI, can be kept locally if desired)
|
||||||
*.dex
|
**/build/
|
||||||
|
|
||||||
# Java class files
|
|
||||||
*.class
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
bin/
|
|
||||||
gen/
|
|
||||||
out/
|
|
||||||
|
|
||||||
# Gradle files
|
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
|
||||||
local.properties
|
|
||||||
|
|
||||||
# Proguard folder generated by Eclipse
|
|
||||||
proguard/
|
|
||||||
|
|
||||||
# Log Files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Android Studio Navigation editor temp files
|
|
||||||
.navigation/
|
|
||||||
|
|
||||||
# Android Studio captures folder
|
|
||||||
captures/
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
*.iml
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Keystore files
|
|
||||||
*.jks
|
|
||||||
*.keystore
|
|
||||||
|
|
||||||
# External native build folder generated in Android Studio 2.2 and later
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx/
|
|
||||||
|
|
||||||
# Google Services (API key)
|
|
||||||
google-services.json
|
|
||||||
|
|
||||||
# Freeline
|
|
||||||
freeline.py
|
|
||||||
freeline/
|
|
||||||
freeline_project_description.json
|
|
||||||
|
|
||||||
# fastjson
|
|
||||||
fastjson/
|
|
||||||
|
|
||||||
# Test folder
|
|
||||||
test/
|
|
||||||
tests/
|
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# Airtest
|
|
||||||
*.log
|
|
||||||
*.png
|
|
||||||
report/
|
|
||||||
|
|||||||
102
README.md
102
README.md
@@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
CheckShot 是一个面向 Android 的图片处理与检查工具,包含水印处理、拼图合成、以及丰富的设置能力,配合 AirTest 自动化测试用例,帮助实现端到端的图像工作流。
|
CheckShot 是一个面向 Android 的图片处理与检查工具,包含水印处理、拼图合成、以及丰富的设置能力,配合 AirTest 自动化测试用例,帮助实现端到端的图像工作流。
|
||||||
|
|
||||||
## 核心功能
|
核心功能
|
||||||
|
|
||||||
- 水印设置
|
- 水印设置
|
||||||
- 时间水印:拍摄后在左下角叠加时间戳,格式固定为 "yyyy年-MM月-dd日 HH:mm:ss"。
|
- 时间水印:拍摄后在左下角叠加时间戳,格式固定为 "yyyy年-MM月-dd日 HH:mm:ss"。
|
||||||
- 地点水印:优先通过 Geocoder 联网解析地址,失败时回落显示经纬度。可在设置中配置校准方式。
|
- 地点水印:优先通过 Geocoder 联网解析地址,失败时回落显示经纬度。可在设置中配置校准方式。
|
||||||
- 样式:提供三种预设样式(默认/简约/醒目),并可在设置中预览和应用。
|
- 样式:提供三种预设样式(默认/简约/醒目),并可在设置中预览和应用。
|
||||||
- 多图拼图(合成)模块
|
- 多图拼图(合成)模块
|
||||||
- 布局规则:支持 2x2 和 3x3 两种网格布局,图片自动缩放裁剪以适配网格。
|
- 布局规则:核心布局为 2x2 网格,且支持扩展布局如 1+3、3+1、1+2、2+1、单图等,图片自动缩放裁剪以适配网格。
|
||||||
- 核心能力:图片拼接、模板化布局编辑(替换/删除图片)、合成质量控制(分辨率/清晰度)。
|
- 核心能力:图片拼接、模板化布局编辑(替换/删除图片)、合成质量控制(分辨率/清晰度)。
|
||||||
- 交互:支持替换网格中的图片、删除图片、添加新图片、设置合成质量和文本水印文本。
|
- 交互:支持替换网格中的图片、删除图片、添加新图片、设置合成质量和文本水印文本。
|
||||||
- 设置与通用配置
|
- 设置与通用配置
|
||||||
@@ -17,112 +16,27 @@ CheckShot 是一个面向 Android 的图片处理与检查工具,包含水印
|
|||||||
- 测试与自动化
|
- 测试与自动化
|
||||||
- 集成 AirTest 测试用例,覆盖水印、相册、拼图、设置等场景,便于回归验证。
|
- 集成 AirTest 测试用例,覆盖水印、相册、拼图、设置等场景,便于回归验证。
|
||||||
|
|
||||||
---
|
架构与实现要点
|
||||||
|
|
||||||
## 通用拼图布局组件技术规格
|
|
||||||
|
|
||||||
### 1. 逻辑结构
|
|
||||||
|
|
||||||
采用“上-中-下”三段式布局:
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────┐
|
|
||||||
│ 标题区 (Title) │ 高度自适应,支持 1 行文本
|
|
||||||
├─────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 图片网格区 │ 2x2 或 3x3 栅格布局
|
|
||||||
│ (Grid Area) │
|
|
||||||
│ │
|
|
||||||
├─────────────────────────┤
|
|
||||||
│ 底部文字区 (Text) │ 200dp 高度,包含标题+内容
|
|
||||||
└─────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 核心布局参数化
|
|
||||||
|
|
||||||
| 参数 | 说明 | 当前值 |
|
|
||||||
|------|------|--------|
|
|
||||||
| Grid_Columns | 列数 | 2 或 3 |
|
|
||||||
| Grid_Rows | 行数 | 2 或 3 |
|
|
||||||
| S | 单元格间距 | 4dp |
|
|
||||||
| P | 容器内边距 | 16dp |
|
|
||||||
|
|
||||||
布局模板自动匹配:根据图片数量自动选择最接近的栅格模板。
|
|
||||||
|
|
||||||
### 3. 内容渲染规则
|
|
||||||
|
|
||||||
- **标题区**:位于顶部,高度自适应,支持 1 行文本
|
|
||||||
- **图片网格区**:位于栅格正下方,由图片组成
|
|
||||||
- **内容区**:位于底部,由 Title(加粗)和 Content(常规)组成
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 缩放算法规格
|
|
||||||
|
|
||||||
#### 等比缩放 (Proportional Scaling)
|
|
||||||
|
|
||||||
- 锁定原始宽高比,严禁 X 或 Y 轴独立拉伸
|
|
||||||
- 计算缩放比例:`k = max(Wt / W_orig, Ht / H_orig)`
|
|
||||||
|
|
||||||
其中:
|
|
||||||
- `Wt` = 目标单元格宽度(物理像素)
|
|
||||||
- `Ht` = 目标单元格高度(物理像素)
|
|
||||||
- `W_orig` = 原始图片宽度
|
|
||||||
- `H_orig` = 原始图片高度
|
|
||||||
|
|
||||||
#### 填充模式
|
|
||||||
|
|
||||||
- **Aspect Fill**:图片须完全覆盖单元格区域,不允许出现留白(Gap-free)
|
|
||||||
|
|
||||||
#### 采样策略
|
|
||||||
|
|
||||||
- **下采样 (Downsampling)**:针对大图缩小,采用双线性(Bilinear)过滤,避免出现锯齿或摩尔纹
|
|
||||||
|
|
||||||
#### 对齐规则
|
|
||||||
|
|
||||||
- **Center Aligned**:初始加载时,图片几何中心与单元格几何中心重合
|
|
||||||
|
|
||||||
#### 分辨率限制
|
|
||||||
|
|
||||||
- **Max DPI 限制**:缩小后的图片逻辑像素密度不低于 320dpi,确保在高分屏下的视觉清晰度
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 布局通用适配规则
|
|
||||||
|
|
||||||
1. 系统根据当前选定的布局模板(2x2 或 3x3),计算出单个单元格的目标物理像素 (Wt, Ht)
|
|
||||||
2. 原始图片输入后,自动计算缩放比例 k = max(Wt/W_orig, Ht/H_orig)
|
|
||||||
3. 图片按 k 倍数进行等比缩小
|
|
||||||
4. 缩小后的图片根据单元格中心进行溢出裁切,确保视觉上的严丝合缝
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 架构与实现要点
|
|
||||||
|
|
||||||
- Watermark:时间文本来自系统时间,地点文本通过 LocationHelper/Geocoder 获取。
|
- Watermark:时间文本来自系统时间,地点文本通过 LocationHelper/Geocoder 获取。
|
||||||
- Puzzle:通过 Bitmap 拼接与 Canvas/Matrix 实现多种布局,支持图片替换、删除与质量控制。
|
- Puzzle:通过 Bitmap 拼接与 Canvas/Matrix 实现多种布局,支持图片替换、删除与质量控制。
|
||||||
- Settings:使用 DataStore 保存用户偏好。
|
- Settings:使用 DataStore 保存用户偏好。
|
||||||
|
|
||||||
## 快速开始
|
快速开始
|
||||||
|
|
||||||
- 构建与运行
|
- 构建与运行
|
||||||
- 构建调试 APK:`./gradlew assembleDebug`
|
- 构建调试 APK:`./gradlew assembleDebug`
|
||||||
- 在设备/模拟器上安装:`adb install -r app/build/outputs/apk/debug/app-debug.apk`
|
- 在设备/模拟器上安装:`adb install -r app/build/outputs/apk/debug/app-debug.apk`
|
||||||
- AirTest 测试
|
- AirTest 测试
|
||||||
- 运行测试用例(需安装 AirTest 及依赖):`airtest run test/airtest/... --device Android:///`
|
- 运行测试用例(需安装 AirTest 及依赖):`airtest run test/airtest/... --device Android:///`
|
||||||
|
|
||||||
## 项目结构与升级
|
|
||||||
|
|
||||||
|
项目结构与升级
|
||||||
- 主要代码位于:
|
- 主要代码位于:
|
||||||
- app/src/main/... 业务代码与 UI 组件
|
- app/src/main/... 业务代码与 UI 组件
|
||||||
- app/src/test/... 单元测试
|
- app/src/test/... 单元测试
|
||||||
- test/airtest/... AirTest 的 UI 测试用例
|
- test/airtest/... AirTest 的 UI 测试用例
|
||||||
- README 作为版本演进的记录,后续变更请同步更新。
|
- README 作为版本演进的记录,后续变更请同步更新。
|
||||||
|
|
||||||
## 持续集成与发布
|
持续集成与发布
|
||||||
|
|
||||||
- 建议在 CI 中执行:编译、单元测试、AirTest 测试并在通过后打包发行版。
|
- 建议在 CI 中执行:编译、单元测试、AirTest 测试并在通过后打包发行版。
|
||||||
|
|
||||||
## 联系与贡献
|
联系与贡献
|
||||||
|
|
||||||
- 如需进一步定制文档结构,或需要将某些部分拆分为单独的开发指南,请告知偏好。
|
- 如需进一步定制文档结构,或需要将某些部分拆分为单独的开发指南,请告知偏好。
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ enum class ImageQuality(val quality: Int, val displayName: String) {
|
|||||||
/**
|
/**
|
||||||
* 合成布局类型
|
* 合成布局类型
|
||||||
*/
|
*/
|
||||||
enum class MergeLayoutType(val rows: Int, val cols: Int, val displayName: String, val maxImages: Int) {
|
enum class MergeLayoutType(val rows: Int, val cols: Int, val displayName: String) {
|
||||||
Grid2x2(2, 2, "2x2网格", 4),
|
Grid2x2(2, 2, "2x2网格"),
|
||||||
Grid3x3(3, 3, "3x3网格", 9)
|
Grid1x3(1, 3, "1+3布局"),
|
||||||
|
Grid3x1(3, 1, "3+1布局")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package com.inspection.camera.ui.camera
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -76,7 +74,6 @@ import com.inspection.camera.util.PermissionManager
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@@ -122,23 +119,6 @@ fun CameraScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有权限被永久拒绝
|
|
||||||
val hasPermanentlyDeniedPermission = remember(permissionsState) {
|
|
||||||
permissionsState.permissions.any { permissionState ->
|
|
||||||
val status = permissionState.status
|
|
||||||
status is com.google.accompanist.permissions.PermissionStatus.Denied &&
|
|
||||||
!status.shouldShowRationale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开应用设置页面
|
|
||||||
val openAppSettings = {
|
|
||||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.parse("package:${context.packageName}")
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
preferencesManager.watermarkStyle.collect { style ->
|
preferencesManager.watermarkStyle.collect { style ->
|
||||||
@@ -159,17 +139,17 @@ fun CameraScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取位置(10秒超时)
|
// 获取位置
|
||||||
LaunchedEffect(permissionsState.allPermissionsGranted, hasLocationPermission) {
|
LaunchedEffect(permissionsState.allPermissionsGranted, hasLocationPermission) {
|
||||||
isLocationLoading = true
|
isLocationLoading = true
|
||||||
if (permissionsState.allPermissionsGranted && hasLocationPermission) {
|
if (permissionsState.allPermissionsGranted && hasLocationPermission) {
|
||||||
try {
|
try {
|
||||||
Log.d("CameraScreen", "Getting location with 10s timeout...")
|
Log.d("CameraScreen", "Getting location...")
|
||||||
val result = withTimeoutOrNull(10000) {
|
locationText = locationHelper.getLocationInfo()
|
||||||
locationHelper.getLocationInfo()
|
|
||||||
}
|
|
||||||
locationText = result ?: "定位失败"
|
|
||||||
Log.d("CameraScreen", "Location result: $locationText")
|
Log.d("CameraScreen", "Location result: $locationText")
|
||||||
|
if (locationText.isEmpty()) {
|
||||||
|
locationText = "定位失败"
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("CameraScreen", "Location error", e)
|
Log.e("CameraScreen", "Location error", e)
|
||||||
locationText = "定位失败"
|
locationText = "定位失败"
|
||||||
@@ -197,13 +177,10 @@ fun CameraScreen(
|
|||||||
flashMode = flashMode,
|
flashMode = flashMode,
|
||||||
watermarkStyle = currentWatermarkStyle,
|
watermarkStyle = currentWatermarkStyle,
|
||||||
imageQuality = currentImageQuality,
|
imageQuality = currentImageQuality,
|
||||||
locationText = getValidLocationTextForPhoto(locationText, manualAddress, locationHelper),
|
locationText = if (locationText.isNotBlank()) locationText else "未知地点",
|
||||||
onComplete = { uri ->
|
onComplete = { uri ->
|
||||||
capturedImages.add(uri)
|
capturedImages.add(uri)
|
||||||
isCapturing = false
|
isCapturing = false
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
isCapturing = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -221,9 +198,7 @@ fun CameraScreen(
|
|||||||
PermissionRequest(
|
PermissionRequest(
|
||||||
onRequestPermission = { permissionsState.launchMultiplePermissionRequest() },
|
onRequestPermission = { permissionsState.launchMultiplePermissionRequest() },
|
||||||
showDialog = showPermissionDeniedDialog,
|
showDialog = showPermissionDeniedDialog,
|
||||||
onDismissDialog = { showPermissionDeniedDialog = false },
|
onDismissDialog = { showPermissionDeniedDialog = false }
|
||||||
hasPermanentlyDeniedPermission = hasPermanentlyDeniedPermission,
|
|
||||||
openAppSettings = openAppSettings
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +278,7 @@ private fun CameraContent(
|
|||||||
|
|
||||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cameraProvider.unbindAll()
|
cameraProvider.unbindAll()
|
||||||
cameraProvider.bindToLifecycle(
|
cameraProvider.bindToLifecycle(
|
||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
@@ -311,10 +286,8 @@ private fun CameraContent(
|
|||||||
preview,
|
preview,
|
||||||
imageCapture
|
imageCapture
|
||||||
)
|
)
|
||||||
Log.d("CameraScreen", "Camera initialized successfully")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("CameraScreen", "Camera binding failed", e)
|
Log.e("CameraScreen", "Camera binding failed", e)
|
||||||
imageCapture = null
|
|
||||||
}
|
}
|
||||||
}, ContextCompat.getMainExecutor(context))
|
}, ContextCompat.getMainExecutor(context))
|
||||||
|
|
||||||
@@ -427,9 +400,7 @@ private fun BottomControls(
|
|||||||
private fun PermissionRequest(
|
private fun PermissionRequest(
|
||||||
onRequestPermission: () -> Unit,
|
onRequestPermission: () -> Unit,
|
||||||
showDialog: Boolean,
|
showDialog: Boolean,
|
||||||
onDismissDialog: () -> Unit,
|
onDismissDialog: () -> Unit
|
||||||
hasPermanentlyDeniedPermission: Boolean,
|
|
||||||
openAppSettings: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -444,57 +415,16 @@ private fun PermissionRequest(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (hasPermanentlyDeniedPermission)
|
text = "请授予权限以使用拍照和地点水印功能",
|
||||||
"权限被永久拒绝,请在设置中手动开启权限"
|
|
||||||
else
|
|
||||||
"请授予权限以使用拍照和地点水印功能",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
if (hasPermanentlyDeniedPermission) {
|
Button(onClick = onRequestPermission) {
|
||||||
Button(onClick = openAppSettings) {
|
Text("授予权限")
|
||||||
Text("打开设置")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button(onClick = onRequestPermission) {
|
|
||||||
Text("授予权限")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getValidLocationTextForPhoto(
|
|
||||||
currentLocationText: String,
|
|
||||||
manualAddress: String,
|
|
||||||
locationHelper: LocationHelper
|
|
||||||
): String {
|
|
||||||
Log.d("CameraScreen", "getValidLocationTextForPhoto called, currentLocationText=$currentLocationText")
|
|
||||||
|
|
||||||
// 检查当前定位文本是否有效
|
|
||||||
val invalidTexts = listOf("正在定位...", "定位失败", "请授予定位权限")
|
|
||||||
val isInvalid = currentLocationText.isBlank() || invalidTexts.contains(currentLocationText)
|
|
||||||
|
|
||||||
if (!isInvalid) {
|
|
||||||
Log.d("CameraScreen", "Using current location text: $currentLocationText")
|
|
||||||
return currentLocationText
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用手动地址
|
|
||||||
if (manualAddress.isNotBlank()) {
|
|
||||||
Log.d("CameraScreen", "Using manual address: $manualAddress")
|
|
||||||
return manualAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试快速获取当前位置(使用缓存)
|
|
||||||
Log.d("CameraScreen", "Requesting new location...")
|
|
||||||
val location = locationHelper.getCurrentLocation()
|
|
||||||
val result = location?.let {
|
|
||||||
"${"%.4f".format(it.latitude)}, ${"%.4f".format(it.longitude)}"
|
|
||||||
} ?: "未知地点"
|
|
||||||
Log.d("CameraScreen", "Got location result: $result")
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun capturePhoto(
|
private fun capturePhoto(
|
||||||
context: Context,
|
context: Context,
|
||||||
imageCapture: ImageCapture,
|
imageCapture: ImageCapture,
|
||||||
@@ -502,10 +432,8 @@ private fun capturePhoto(
|
|||||||
watermarkStyle: WatermarkStyle,
|
watermarkStyle: WatermarkStyle,
|
||||||
imageQuality: ImageQuality,
|
imageQuality: ImageQuality,
|
||||||
locationText: String,
|
locationText: String,
|
||||||
onComplete: (Uri) -> Unit,
|
onComplete: (Uri) -> Unit
|
||||||
onError: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
Log.d("CameraScreen", "capturePhoto called, locationText=$locationText")
|
|
||||||
val photoFile = File(
|
val photoFile = File(
|
||||||
context.cacheDir,
|
context.cacheDir,
|
||||||
"photo_${System.currentTimeMillis()}.jpg"
|
"photo_${System.currentTimeMillis()}.jpg"
|
||||||
@@ -519,17 +447,9 @@ private fun capturePhoto(
|
|||||||
executor,
|
executor,
|
||||||
object : ImageCapture.OnImageSavedCallback {
|
object : ImageCapture.OnImageSavedCallback {
|
||||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||||
Log.d("CameraScreen", "Photo saved, adding watermark with locationText=$locationText")
|
|
||||||
val bitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
|
val bitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
|
||||||
if (bitmap == null) {
|
if (bitmap != null) {
|
||||||
Log.e("CameraScreen", "Failed to decode photo file: ${photoFile.absolutePath}")
|
|
||||||
onError()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val timeText = ImageProcessor.getCurrentTimeText()
|
val timeText = ImageProcessor.getCurrentTimeText()
|
||||||
Log.d("CameraScreen", "Adding watermark: timeText=$timeText, locationText=$locationText")
|
|
||||||
val watermarkedBitmap = ImageProcessor.addWatermark(
|
val watermarkedBitmap = ImageProcessor.addWatermark(
|
||||||
bitmap,
|
bitmap,
|
||||||
timeText,
|
timeText,
|
||||||
@@ -544,22 +464,12 @@ private fun capturePhoto(
|
|||||||
bitmap.recycle()
|
bitmap.recycle()
|
||||||
watermarkedBitmap.recycle()
|
watermarkedBitmap.recycle()
|
||||||
|
|
||||||
if (uri != null) {
|
uri?.let { onComplete(it) }
|
||||||
onComplete(uri)
|
|
||||||
} else {
|
|
||||||
Log.e("CameraScreen", "Failed to save image to gallery")
|
|
||||||
onError()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("CameraScreen", "Error processing image", e)
|
|
||||||
bitmap.recycle()
|
|
||||||
onError()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(exception: ImageCaptureException) {
|
override fun onError(exception: ImageCaptureException) {
|
||||||
Log.e("CameraScreen", "Photo capture failed", exception)
|
Log.e("CameraScreen", "Photo capture failed", exception)
|
||||||
onError()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ fun GalleryScreen(
|
|||||||
var selectedImages by remember { mutableStateOf<Set<Uri>>(emptySet()) }
|
var selectedImages by remember { mutableStateOf<Set<Uri>>(emptySet()) }
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
var isSelectionMode by remember { mutableStateOf(false) }
|
var isSelectionMode by remember { mutableStateOf(false) }
|
||||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
|
||||||
|
|
||||||
// 加载图片
|
// 加载图片
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -150,9 +149,6 @@ fun GalleryScreen(
|
|||||||
} else {
|
} else {
|
||||||
selectedImages + uri
|
selectedImages + uri
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 非选择模式下,点击打开大图查看器
|
|
||||||
selectedImageUri = uri
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
@@ -193,32 +189,6 @@ fun GalleryScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 大图查看对话框
|
|
||||||
if (selectedImageUri != null) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { selectedImageUri = null },
|
|
||||||
title = { Text("图片预览") },
|
|
||||||
text = {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(selectedImageUri)
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
contentDescription = "大图预览",
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(1f)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = { selectedImageUri = null }) {
|
|
||||||
Text("关闭")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -2,492 +2,37 @@ package com.inspection.camera.ui.merge
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
// java.io.File and FileOutputStream will be referenced with fully qualified names to avoid ambiguity
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import coil.request.ImageRequest
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import com.inspection.camera.data.PreferencesManager
|
import com.inspection.camera.util.PuzzleMerge
|
||||||
import com.inspection.camera.data.models.ImageItem
|
|
||||||
import com.inspection.camera.data.models.ImageQuality
|
|
||||||
import com.inspection.camera.data.models.MergeLayoutType
|
|
||||||
import com.inspection.camera.data.models.WatermarkStyle
|
|
||||||
import com.inspection.camera.ui.theme.Primary
|
|
||||||
import com.inspection.camera.util.ImageProcessor
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MergeScreen(
|
fun MergeScreen(imageUris: List<Uri>) {
|
||||||
imageUris: List<Uri>,
|
|
||||||
onNavigateBack: () -> Unit,
|
|
||||||
onMergeComplete: (Uri) -> Unit,
|
|
||||||
preferencesManager: PreferencesManager
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
var mergedBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
data class ImageWithCache(val uri: Uri, val cachePath: String?)
|
LaunchedEffect(imageUris) {
|
||||||
|
mergedBitmap = PuzzleMerge.mergeToBitmap(context, imageUris.take(4), 1000)
|
||||||
val images = remember { mutableStateListOf<ImageWithCache>() }
|
|
||||||
var layoutType by remember { mutableStateOf(MergeLayoutType.Grid2x2) }
|
|
||||||
var imageQuality by remember { mutableStateOf(ImageQuality.Standard) }
|
|
||||||
var titleStyle by remember { mutableStateOf(WatermarkStyle.Default) }
|
|
||||||
var contentStyle by remember { mutableStateOf(WatermarkStyle.Default) }
|
|
||||||
var showPreview by remember { mutableStateOf(false) }
|
|
||||||
var previewBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
|
||||||
var title by remember { mutableStateOf("") }
|
|
||||||
var content by remember { mutableStateOf("") }
|
|
||||||
var showSaveDialog by remember { mutableStateOf(false) }
|
|
||||||
var selectedImageIndex by remember { mutableStateOf(-1) }
|
|
||||||
|
|
||||||
// 将 URI 图片复制到缓存目录
|
|
||||||
suspend fun copyImageToCache(uri: android.net.Uri): String? {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val input = context.contentResolver.openInputStream(uri) ?: return@withContext null
|
|
||||||
val cacheDir = context.cacheDir
|
|
||||||
val imgDir = java.io.File(cacheDir, "merge_images")
|
|
||||||
if (!imgDir.exists()) {
|
|
||||||
imgDir.mkdirs()
|
|
||||||
}
|
|
||||||
val fileName = "img_${System.nanoTime()}.jpg"
|
|
||||||
val outFile = java.io.File(imgDir, fileName)
|
|
||||||
java.io.FileOutputStream(outFile).use { output ->
|
|
||||||
input.use { inStream -> inStream.copyTo(output) }
|
|
||||||
}
|
|
||||||
outFile.absolutePath
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图片选择器
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
val imagePickerLauncher = rememberLauncherForActivityResult(
|
mergedBitmap?.let { bmp ->
|
||||||
contract = ActivityResultContracts.GetMultipleContents()
|
Image(
|
||||||
) { uris ->
|
bitmap = bmp.asImageBitmap(),
|
||||||
if (uris.isNotEmpty()) {
|
contentDescription = "拼图合成预览",
|
||||||
scope.launch {
|
modifier = Modifier
|
||||||
if (selectedImageIndex >= 0 && selectedImageIndex < images.size) {
|
.fillMaxSize()
|
||||||
// 替换指定位置的图片
|
|
||||||
val path = copyImageToCache(uris.first())
|
|
||||||
if (path != null) {
|
|
||||||
images[selectedImageIndex] = ImageWithCache(uris.first(), path)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 添加新图片
|
|
||||||
uris.take(layoutType.maxImages - images.size).forEach { uri ->
|
|
||||||
val path = copyImageToCache(uri)
|
|
||||||
if (path != null) {
|
|
||||||
images.add(ImageWithCache(uri, path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedImageIndex = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载用户配置
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
preferencesManager.mergeLayout.collect { layoutType = it }
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
preferencesManager.imageQuality.collect { imageQuality = it }
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
preferencesManager.titleStyle.collect { titleStyle = it }
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
preferencesManager.contentStyle.collect { contentStyle = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text("图片合成") },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onNavigateBack) {
|
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "返回")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = Primary,
|
|
||||||
titleContentColor = Color.White,
|
|
||||||
navigationIconContentColor = Color.White
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
) {
|
|
||||||
// 布局选择
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
MergeLayoutType.entries.forEach { layout ->
|
|
||||||
LayoutOption(
|
|
||||||
layout = layout,
|
|
||||||
isSelected = layoutType == layout,
|
|
||||||
onClick = {
|
|
||||||
layoutType = layout
|
|
||||||
if (images.size > layout.maxImages) {
|
|
||||||
while (images.size > layout.maxImages) {
|
|
||||||
images.removeAt(images.size - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 质量选择
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text("质量:", style = MaterialTheme.typography.bodySmall)
|
|
||||||
ImageQuality.entries.forEach { quality ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.background(if (imageQuality == quality) Primary else Color.LightGray)
|
|
||||||
.clickable { imageQuality = quality }
|
|
||||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = quality.displayName,
|
|
||||||
color = if (imageQuality == quality) Color.White else Color.Black,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图片网格
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Fixed(layoutType.cols),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
itemsIndexed(images) { index, imageWithCache ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.aspectRatio(1f)
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(Color.LightGray)
|
|
||||||
.clickable {
|
|
||||||
selectedImageIndex = index
|
|
||||||
imagePickerLauncher.launch("image/*")
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(context)
|
|
||||||
.data(imageWithCache.uri)
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
|
|
||||||
// 删除按钮
|
|
||||||
IconButton(
|
|
||||||
onClick = { images.removeAt(index) },
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.size(32.dp)
|
|
||||||
.background(Color.Black.copy(alpha = 0.5f), CircleShape)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Close,
|
|
||||||
contentDescription = "删除",
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 替换图标
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Refresh,
|
|
||||||
contentDescription = "替换",
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.size(32.dp)
|
|
||||||
.padding(4.dp)
|
|
||||||
.background(Color.Black.copy(alpha = 0.5f), CircleShape)
|
|
||||||
.padding(4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加图片按钮
|
|
||||||
if (images.size < layoutType.maxImages) {
|
|
||||||
item {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.aspectRatio(1f)
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(Color.LightGray.copy(alpha = 0.5f))
|
|
||||||
.clickable {
|
|
||||||
imagePickerLauncher.launch("image/*")
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Add,
|
|
||||||
contentDescription = "添加图片",
|
|
||||||
tint = Color.Gray,
|
|
||||||
modifier = Modifier.size(48.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文字编辑区
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = title,
|
|
||||||
onValueChange = { title = it },
|
|
||||||
label = { Text("标题") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = content,
|
|
||||||
onValueChange = { content = it },
|
|
||||||
label = { Text("内容") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
minLines = 2,
|
|
||||||
maxLines = 4
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 底部按钮
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
previewBitmap = withContext(Dispatchers.Default) {
|
|
||||||
val imageItems = images.map { img ->
|
|
||||||
ImageItem(
|
|
||||||
uri = img.uri,
|
|
||||||
path = img.cachePath ?: img.uri.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ImageProcessor.mergeImages(
|
|
||||||
context,
|
|
||||||
imageItems,
|
|
||||||
layoutType,
|
|
||||||
imageQuality,
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
titleStyle,
|
|
||||||
contentStyle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
showPreview = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text("预览")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = { showSaveDialog = true },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
enabled = images.isNotEmpty()
|
|
||||||
) {
|
|
||||||
Text("保存")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预览对话框
|
|
||||||
if (showPreview && previewBitmap != null) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showPreview = false },
|
|
||||||
title = { Text("预览") },
|
|
||||||
text = {
|
|
||||||
Image(
|
|
||||||
bitmap = previewBitmap!!.asImageBitmap(),
|
|
||||||
contentDescription = "预览",
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = { showPreview = false }) {
|
|
||||||
Text("关闭")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存确认对话框
|
|
||||||
if (showSaveDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showSaveDialog = false },
|
|
||||||
title = { Text("保存合成图片") },
|
|
||||||
text = { Text("确定要将合成后的图片保存到相册吗?") },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
scope.launch {
|
|
||||||
val bitmap = withContext(Dispatchers.Default) {
|
|
||||||
val imageItems = images.map { img ->
|
|
||||||
ImageItem(
|
|
||||||
uri = img.uri,
|
|
||||||
path = img.cachePath ?: img.uri.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ImageProcessor.mergeImages(
|
|
||||||
context,
|
|
||||||
imageItems,
|
|
||||||
layoutType,
|
|
||||||
imageQuality,
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
titleStyle,
|
|
||||||
contentStyle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val fileName = ImageProcessor.generateFileName(title.ifBlank { "合成" })
|
|
||||||
val uri = ImageProcessor.saveToGallery(context, bitmap, fileName)
|
|
||||||
uri?.let { onMergeComplete(it) }
|
|
||||||
showSaveDialog = false
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text("保存")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showSaveDialog = false }) {
|
|
||||||
Text("取消")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LayoutOption(
|
|
||||||
layout: MergeLayoutType,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
val displayText = when (layout) {
|
|
||||||
MergeLayoutType.Grid2x2 -> "2x2"
|
|
||||||
MergeLayoutType.Grid3x3 -> "3x3"
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(if (isSelected) Primary else Color.LightGray)
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
text = displayText,
|
|
||||||
color = if (isSelected) Color.White else Color.Black
|
|
||||||
)
|
|
||||||
if (isSelected) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,44 +33,6 @@ object ImageProcessor {
|
|||||||
private val dateFormat = SimpleDateFormat("yyyy年-MM月-dd日 HH:mm:ss", Locale.getDefault())
|
private val dateFormat = SimpleDateFormat("yyyy年-MM月-dd日 HH:mm:ss", Locale.getDefault())
|
||||||
private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault())
|
private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault())
|
||||||
|
|
||||||
/**
|
|
||||||
* 尝试加载图片 Bitmap
|
|
||||||
*/
|
|
||||||
private fun tryLoadBitmap(context: Context, imageItem: ImageItem): Bitmap? {
|
|
||||||
android.util.Log.d("ImageProcessor", "Loading image, URI: ${imageItem.uri}, path: ${imageItem.path}")
|
|
||||||
|
|
||||||
// 优先尝试 URI 加载
|
|
||||||
try {
|
|
||||||
val uri = imageItem.uri
|
|
||||||
android.util.Log.d("ImageProcessor", "Trying to open URI: $uri")
|
|
||||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
||||||
android.util.Log.d("ImageProcessor", "URI loaded bitmap: ${bitmap != null}")
|
|
||||||
return bitmap
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.e("ImageProcessor", "URI load failed: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试从文件路径加载
|
|
||||||
if (imageItem.path.isNotEmpty()) {
|
|
||||||
try {
|
|
||||||
val file = java.io.File(imageItem.path)
|
|
||||||
android.util.Log.d("ImageProcessor", "Trying file: ${file.absolutePath}, exists: ${file.exists()}")
|
|
||||||
if (file.exists()) {
|
|
||||||
val bitmap = BitmapFactory.decodeFile(imageItem.path)
|
|
||||||
android.util.Log.d("ImageProcessor", "File loaded bitmap: ${bitmap != null}")
|
|
||||||
return bitmap
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.e("ImageProcessor", "File load failed: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android.util.Log.w("ImageProcessor", "Failed to load image")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前时间戳文本
|
* 获取当前时间戳文本
|
||||||
*/
|
*/
|
||||||
@@ -99,90 +61,65 @@ object ImageProcessor {
|
|||||||
locationText: String,
|
locationText: String,
|
||||||
style: WatermarkStyle
|
style: WatermarkStyle
|
||||||
): Bitmap {
|
): Bitmap {
|
||||||
android.util.Log.d("ImageProcessor", "addWatermark called, timeText=$timeText, locationText=$locationText")
|
|
||||||
|
|
||||||
val result = sourceBitmap.copy(Bitmap.Config.ARGB_8888, true)
|
val result = sourceBitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||||
val canvas = Canvas(result)
|
val canvas = Canvas(result)
|
||||||
|
|
||||||
// 使用固定的字体大小,基于图片宽度比例
|
|
||||||
val baseFontSize = result.width / 40f // 字体大小为图片宽度的1/40
|
|
||||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
textSize = baseFontSize
|
textSize = style.fontSize * result.density
|
||||||
color = style.textColor.toArgb()
|
color = style.textColor.toArgb()
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
textAlign = Paint.Align.CENTER
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val watermarkText = "$timeText $locationText"
|
val watermarkText = "$timeText $locationText"
|
||||||
val textWidth = paint.measureText(watermarkText)
|
val textWidth = paint.measureText(watermarkText)
|
||||||
val textHeight = paint.fontMetrics.let { it.descent - it.ascent }
|
val textHeight = paint.fontMetrics.let { it.descent - it.ascent }
|
||||||
|
|
||||||
android.util.Log.d("ImageProcessor", "Watermark: width=$textWidth, height=$textHeight, text=$watermarkText")
|
|
||||||
|
|
||||||
// 计算位置(底部中央)
|
// 计算位置(左下角)
|
||||||
val padding = result.width / 30f // 边距为图片宽度的1/30
|
val padding = 20f * result.density
|
||||||
val x = result.width / 2f
|
val x = padding
|
||||||
val y = result.height - padding
|
val y = result.height - padding
|
||||||
|
|
||||||
// 绘制背景
|
// 绘制背景
|
||||||
|
// 通过ARGB判断来决定是否绘制背景,避免混用 Android Color 与 Compose Color 的类型问题
|
||||||
val bgColorInt = style.backgroundColor.toArgb()
|
val bgColorInt = style.backgroundColor.toArgb()
|
||||||
android.util.Log.d("ImageProcessor", "Background color int: $bgColorInt")
|
|
||||||
if (bgColorInt != 0) {
|
if (bgColorInt != 0) {
|
||||||
val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
color = bgColorInt
|
color = bgColorInt
|
||||||
}
|
}
|
||||||
val bgRect = RectF(
|
val bgRect = RectF(
|
||||||
x - textWidth / 2 - padding / 2,
|
x - 10,
|
||||||
y - textHeight - padding / 2,
|
y - textHeight - 10,
|
||||||
x + textWidth / 2 + padding / 2,
|
x + textWidth + 10,
|
||||||
y + padding / 2
|
y + 10
|
||||||
)
|
)
|
||||||
canvas.drawRoundRect(bgRect, 8f, 8f, bgPaint)
|
canvas.drawRoundRect(bgRect, 8f, 8f, bgPaint)
|
||||||
android.util.Log.d("ImageProcessor", "Background drawn at x=$x, y=$y")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制文字
|
// 绘制文字
|
||||||
canvas.drawText(watermarkText, x, y, paint)
|
canvas.drawText(watermarkText, x, y, paint)
|
||||||
android.util.Log.d("ImageProcessor", "Text drawn at x=$x, y=$y")
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 合成多张图片(支持 URI)
|
* 合成多张图片
|
||||||
*/
|
*/
|
||||||
fun mergeImages(
|
fun mergeImages(
|
||||||
context: Context,
|
|
||||||
images: List<ImageItem>,
|
images: List<ImageItem>,
|
||||||
layoutType: MergeLayoutType,
|
layoutType: MergeLayoutType,
|
||||||
quality: ImageQuality,
|
quality: ImageQuality
|
||||||
title: String = "",
|
|
||||||
content: String = "",
|
|
||||||
titleStyle: WatermarkStyle = WatermarkStyle.Default,
|
|
||||||
contentStyle: WatermarkStyle = WatermarkStyle.Default
|
|
||||||
): Bitmap {
|
): Bitmap {
|
||||||
if (images.isEmpty() && title.isBlank() && content.isBlank()) {
|
if (images.isEmpty()) {
|
||||||
return Bitmap.createBitmap(1920, 1080, Bitmap.Config.ARGB_8888)
|
return Bitmap.createBitmap(1920, 1080, Bitmap.Config.ARGB_8888)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cols = layoutType.cols
|
val cols = layoutType.cols
|
||||||
val rows = layoutType.rows
|
val rows = layoutType.rows
|
||||||
|
val imageCount = images.size.coerceAtMost(rows * cols)
|
||||||
|
|
||||||
val outputWidth = 1920
|
val outputWidth = 1920
|
||||||
|
val outputHeight = 1080
|
||||||
val cellWidth = outputWidth / cols
|
val cellWidth = outputWidth / cols
|
||||||
val cellHeight = outputWidth / cols // 保持正方形格子
|
val cellHeight = outputHeight / rows
|
||||||
|
|
||||||
// 底部文字区域高度
|
|
||||||
val textAreaHeight = if (title.isNotBlank() || content.isNotBlank()) {
|
|
||||||
200 // 200像素高度的文字区域
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图片区域高度
|
|
||||||
val imageAreaHeight = cellHeight * rows
|
|
||||||
|
|
||||||
val outputHeight = imageAreaHeight + textAreaHeight
|
|
||||||
|
|
||||||
val result = Bitmap.createBitmap(outputWidth, outputHeight, Bitmap.Config.ARGB_8888)
|
val result = Bitmap.createBitmap(outputWidth, outputHeight, Bitmap.Config.ARGB_8888)
|
||||||
val canvas = Canvas(result)
|
val canvas = Canvas(result)
|
||||||
@@ -190,7 +127,6 @@ object ImageProcessor {
|
|||||||
|
|
||||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
// 绘制图片网格
|
|
||||||
images.forEachIndexed { index, imageItem ->
|
images.forEachIndexed { index, imageItem ->
|
||||||
if (index >= rows * cols) return@forEachIndexed
|
if (index >= rows * cols) return@forEachIndexed
|
||||||
|
|
||||||
@@ -201,10 +137,10 @@ object ImageProcessor {
|
|||||||
val top = row * cellHeight
|
val top = row * cellHeight
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val sourceBitmap = tryLoadBitmap(context, imageItem)
|
val sourceBitmap = BitmapFactory.decodeFile(imageItem.path)
|
||||||
|
?: return@forEachIndexed
|
||||||
sourceBitmap ?: return@forEachIndexed
|
|
||||||
|
|
||||||
|
// 缩放并居中裁剪
|
||||||
val scaledBitmap = scaleAndCropBitmap(sourceBitmap, cellWidth, cellHeight)
|
val scaledBitmap = scaleAndCropBitmap(sourceBitmap, cellWidth, cellHeight)
|
||||||
val dstRect = Rect(left, top, left + cellWidth, top + cellHeight)
|
val dstRect = Rect(left, top, left + cellWidth, top + cellHeight)
|
||||||
canvas.drawBitmap(scaledBitmap, null, dstRect, paint)
|
canvas.drawBitmap(scaledBitmap, null, dstRect, paint)
|
||||||
@@ -214,6 +150,7 @@ object ImageProcessor {
|
|||||||
}
|
}
|
||||||
sourceBitmap.recycle()
|
sourceBitmap.recycle()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
// 加载失败绘制占位
|
||||||
val placeholderPaint = Paint().apply {
|
val placeholderPaint = Paint().apply {
|
||||||
color = Color.LTGRAY
|
color = Color.LTGRAY
|
||||||
}
|
}
|
||||||
@@ -224,97 +161,6 @@ object ImageProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制底部文字区域
|
|
||||||
if (textAreaHeight > 0) {
|
|
||||||
val textTop = imageAreaHeight
|
|
||||||
|
|
||||||
// 绘制白色背景
|
|
||||||
canvas.drawRect(
|
|
||||||
0f, textTop.toFloat(),
|
|
||||||
outputWidth.toFloat(), outputHeight.toFloat(),
|
|
||||||
Paint().apply { color = Color.WHITE }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 绘制分割线
|
|
||||||
canvas.drawLine(
|
|
||||||
0f, textTop.toFloat(),
|
|
||||||
outputWidth.toFloat(), textTop.toFloat(),
|
|
||||||
Paint().apply {
|
|
||||||
color = Color.LTGRAY
|
|
||||||
strokeWidth = 2f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 绘制标题
|
|
||||||
if (title.isNotBlank()) {
|
|
||||||
val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
||||||
textSize = titleStyle.fontSize * 2f // 放大标题
|
|
||||||
color = titleStyle.textColor.toArgb()
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
textAlign = Paint.Align.CENTER
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标题背景
|
|
||||||
val titleBgPaint = Paint().apply {
|
|
||||||
color = titleStyle.backgroundColor.toArgb()
|
|
||||||
}
|
|
||||||
val titleWidth = titlePaint.measureText(title)
|
|
||||||
val titleHeight = titlePaint.fontMetrics.let { it.descent - it.ascent }
|
|
||||||
val titleBgRect = RectF(
|
|
||||||
(outputWidth - titleWidth) / 2 - 20,
|
|
||||||
textTop + 20f,
|
|
||||||
(outputWidth + titleWidth) / 2 + 20,
|
|
||||||
textTop + 20f + titleHeight + 20
|
|
||||||
)
|
|
||||||
canvas.drawRoundRect(titleBgRect, 8f, 8f, titleBgPaint)
|
|
||||||
|
|
||||||
// 标题文字
|
|
||||||
canvas.drawText(title, outputWidth / 2f, textTop + 20f + titleHeight, titlePaint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容
|
|
||||||
if (content.isNotBlank()) {
|
|
||||||
val contentPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
||||||
textSize = contentStyle.fontSize * 1.5f // 放大内容
|
|
||||||
color = contentStyle.textColor.toArgb()
|
|
||||||
typeface = Typeface.DEFAULT
|
|
||||||
}
|
|
||||||
|
|
||||||
val padding = 40f
|
|
||||||
val contentMaxWidth = outputWidth - padding * 2
|
|
||||||
val contentLines = wrapText(content, contentPaint, contentMaxWidth)
|
|
||||||
val lineHeight = contentPaint.fontMetrics.let { it.descent - it.ascent }
|
|
||||||
|
|
||||||
// 内容背景
|
|
||||||
val contentBgPaint = Paint().apply {
|
|
||||||
color = contentStyle.backgroundColor.toArgb()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从标题下方开始绘制内容
|
|
||||||
val contentStartY = if (title.isNotBlank()) {
|
|
||||||
textTop + 80f + lineHeight
|
|
||||||
} else {
|
|
||||||
textTop + 20f + lineHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
val totalContentHeight = contentLines.size * lineHeight + 20
|
|
||||||
val contentBgRect = RectF(
|
|
||||||
padding - 10,
|
|
||||||
contentStartY - lineHeight - 10,
|
|
||||||
outputWidth - padding + 10,
|
|
||||||
contentStartY + totalContentHeight
|
|
||||||
)
|
|
||||||
canvas.drawRoundRect(contentBgRect, 8f, 8f, contentBgPaint)
|
|
||||||
|
|
||||||
// 内容文字
|
|
||||||
var y = contentStartY
|
|
||||||
contentLines.forEach { line ->
|
|
||||||
canvas.drawText(line, padding, y, contentPaint)
|
|
||||||
y += lineHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ import com.google.android.gms.location.LocationRequest
|
|||||||
import com.google.android.gms.location.LocationResult
|
import com.google.android.gms.location.LocationResult
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
import com.google.android.gms.location.Priority
|
import com.google.android.gms.location.Priority
|
||||||
import android.location.LocationManager
|
|
||||||
import android.location.LocationListener
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
@@ -33,157 +30,14 @@ class LocationHelper(private val context: Context) {
|
|||||||
Geocoder(context, Locale.getDefault())
|
Geocoder(context, Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 位置缓存
|
|
||||||
private var lastLocation: Location? = null
|
|
||||||
private var lastLocationTime: Long = 0
|
|
||||||
private val LOCATION_CACHE_VALID_MS = 30000 // 30秒缓存有效
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查定位服务是否启用(GPS或网络)
|
* 获取当前位置
|
||||||
*/
|
|
||||||
fun isLocationEnabled(): Boolean {
|
|
||||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
||||||
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
|
|
||||||
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前位置(带缓存)
|
|
||||||
*/
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
suspend fun getCurrentLocation(): Location? {
|
suspend fun getCurrentLocation(): Location? {
|
||||||
// 检查缓存是否有效
|
|
||||||
if (lastLocation != null && System.currentTimeMillis() - lastLocationTime < LOCATION_CACHE_VALID_MS) {
|
|
||||||
Log.d("LocationHelper", "Using cached location: $lastLocation")
|
|
||||||
return lastLocation
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查定位服务是否启用
|
|
||||||
if (!isLocationEnabled()) {
|
|
||||||
Log.w("LocationHelper", "Location services disabled")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
var result: Location? = null
|
|
||||||
|
|
||||||
// 并行尝试多种定位方式,使用最快的
|
|
||||||
result = tryHmsLocation()
|
|
||||||
if (result == null) {
|
|
||||||
result = tryGmsLocation()
|
|
||||||
}
|
|
||||||
if (result == null) {
|
|
||||||
result = getNetworkLocationFallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新缓存
|
|
||||||
if (result != null) {
|
|
||||||
lastLocation = result
|
|
||||||
lastLocationTime = System.currentTimeMillis()
|
|
||||||
Log.d("LocationHelper", "Location obtained and cached: $result")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
private suspend fun tryGmsLocation(): Location? {
|
|
||||||
return try {
|
return try {
|
||||||
Log.d("LocationHelper", "Requesting GMS last location...")
|
fusedLocationClient.lastLocation.await()
|
||||||
// 先尝试获取最后已知位置(最快)
|
|
||||||
val lastLocation = fusedLocationClient.lastLocation.await()
|
|
||||||
if (lastLocation != null) {
|
|
||||||
Log.d("LocationHelper", "Got GMS last location: $lastLocation")
|
|
||||||
return lastLocation
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最后位置为空,请求新位置(快速低精度)
|
|
||||||
Log.d("LocationHelper", "Last location null, requesting fresh location...")
|
|
||||||
requestFastLocation()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("LocationHelper", "GMS location failed", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMS 位置服务反射实现(无编译时依赖)
|
|
||||||
@Suppress("MissingPermission")
|
|
||||||
private suspend fun tryHmsLocation(): Location? {
|
|
||||||
Log.d("LocationHelper", "Attempt HMS location via reflection...")
|
|
||||||
return try {
|
|
||||||
val servicesClass = Class.forName("com.huawei.hms.location.LocationServices")
|
|
||||||
val getClient = servicesClass.getMethod("getFusedLocationProviderClient", Context::class.java)
|
|
||||||
val client = getClient.invoke(null, context)
|
|
||||||
val clientClass = client!!.javaClass
|
|
||||||
val getLastLocation = clientClass.getMethod("getLastLocation")
|
|
||||||
val task = getLastLocation.invoke(client)
|
|
||||||
val taskClass = task!!.javaClass
|
|
||||||
val latch = java.util.concurrent.CountDownLatch(1)
|
|
||||||
var result: Location? = null
|
|
||||||
val onSuccess = java.lang.reflect.Proxy.newProxyInstance(
|
|
||||||
LocationHelper::class.java.classLoader,
|
|
||||||
arrayOf(Class.forName("com.huawei.hmf.tasks.OnSuccessListener")),
|
|
||||||
java.lang.reflect.InvocationHandler { _, method, args ->
|
|
||||||
if (method.name == "onSuccess" && args != null) {
|
|
||||||
result = args[0] as Location
|
|
||||||
latch.countDown()
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val onFailure = java.lang.reflect.Proxy.newProxyInstance(
|
|
||||||
LocationHelper::class.java.classLoader,
|
|
||||||
arrayOf(Class.forName("com.huawei.hmf.tasks.OnFailureListener")),
|
|
||||||
java.lang.reflect.InvocationHandler { _, method, args ->
|
|
||||||
if (method.name == "onFailure" && args != null) {
|
|
||||||
latch.countDown()
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val addSuccess = taskClass.getMethod("addOnSuccessListener", Class.forName("com.huawei.hmf.tasks.OnSuccessListener"))
|
|
||||||
val addFailure = taskClass.getMethod("addOnFailureListener", Class.forName("com.huawei.hmf.tasks.OnFailureListener"))
|
|
||||||
addSuccess.invoke(task, onSuccess)
|
|
||||||
addFailure.invoke(task, onFailure)
|
|
||||||
latch.await(5000, java.util.concurrent.TimeUnit.MILLISECONDS)
|
|
||||||
result
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e("LocationHelper", "HMS location reflection failed", t)
|
|
||||||
Log.e("LocationHelper", "HMS location fallback failed", t)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
private suspend fun requestFastLocation(): Location? {
|
|
||||||
return try {
|
|
||||||
Log.d("LocationHelper", "Creating fast location request...")
|
|
||||||
// 使用平衡功耗精度,更快获取位置
|
|
||||||
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 5000)
|
|
||||||
.setWaitForAccurateLocation(false)
|
|
||||||
.setMinUpdateIntervalMillis(1000)
|
|
||||||
.setMaxUpdateDelayMillis(8000)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
withTimeoutOrNull(8000) {
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
Log.d("LocationHelper", "Requesting fast location updates...")
|
|
||||||
val callback = object : LocationCallback() {
|
|
||||||
override fun onLocationResult(result: LocationResult) {
|
|
||||||
Log.d("LocationHelper", "Got fast location result: ${result.lastLocation}")
|
|
||||||
fusedLocationClient.removeLocationUpdates(this)
|
|
||||||
continuation.resume(result.lastLocation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fusedLocationClient.requestLocationUpdates(locationRequest, callback, Looper.getMainLooper())
|
|
||||||
|
|
||||||
continuation.invokeOnCancellation {
|
|
||||||
Log.d("LocationHelper", "Fast location request cancelled")
|
|
||||||
fusedLocationClient.removeLocationUpdates(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("LocationHelper", "Request fast location failed", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,9 +48,7 @@ class LocationHelper(private val context: Context) {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
suspend fun getAddressFromLocation(latitude: Double, longitude: Double): String {
|
suspend fun getAddressFromLocation(latitude: Double, longitude: Double): String {
|
||||||
return try {
|
return try {
|
||||||
Log.d("LocationHelper", "Getting address for: $latitude, $longitude")
|
|
||||||
val addresses = geocoder.getFromLocation(latitude, longitude, 1)
|
val addresses = geocoder.getFromLocation(latitude, longitude, 1)
|
||||||
Log.d("LocationHelper", "Geocoder results: $addresses")
|
|
||||||
if (!addresses.isNullOrEmpty()) {
|
if (!addresses.isNullOrEmpty()) {
|
||||||
val address = addresses[0]
|
val address = addresses[0]
|
||||||
buildString {
|
buildString {
|
||||||
@@ -208,7 +60,6 @@ class LocationHelper(private val context: Context) {
|
|||||||
"${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}"
|
"${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}"
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("LocationHelper", "Geocoder error", e)
|
|
||||||
"${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}"
|
"${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,33 +79,6 @@ class LocationHelper(private val context: Context) {
|
|||||||
Log.d("LocationHelper", "Network location: $location")
|
Log.d("LocationHelper", "Network location: $location")
|
||||||
return location?.let { getAddressFromLocation(it.latitude, it.longitude) } ?: ""
|
return location?.let { getAddressFromLocation(it.latitude, it.longitude) } ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// 备选方案:当设备没有 Google Play Services 时,尝试使用 Android 原生 LocationManager 获取网络定位
|
|
||||||
internal suspend fun getNetworkLocationFallback(): Location? {
|
|
||||||
return try {
|
|
||||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
||||||
val provider = LocationManager.NETWORK_PROVIDER
|
|
||||||
val enabled = locationManager.isProviderEnabled(provider)
|
|
||||||
if (!enabled) return null
|
|
||||||
|
|
||||||
suspendCancellableCoroutine { cont ->
|
|
||||||
val listener = object : LocationListener {
|
|
||||||
override fun onLocationChanged(location: Location) {
|
|
||||||
locationManager.removeUpdates(this)
|
|
||||||
cont.resume(location)
|
|
||||||
}
|
|
||||||
override fun onStatusChanged(provider: String?, status: Int, extras: android.os.Bundle?) { /* no-op */ }
|
|
||||||
override fun onProviderEnabled(provider: String) { /* no-op */ }
|
|
||||||
override fun onProviderDisabled(provider: String) { /* no-op */ }
|
|
||||||
}
|
|
||||||
locationManager.requestLocationUpdates(provider, 0L, 0f, listener, Looper.getMainLooper())
|
|
||||||
cont.invokeOnCancellation { locationManager.removeUpdates(listener) }
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("LocationHelper", "Network fallback location failed", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T> com.google.android.gms.tasks.Task<T>.await(): T {
|
private suspend fun <T> com.google.android.gms.tasks.Task<T>.await(): T {
|
||||||
|
|||||||
59
app/src/main/java/com/inspection/camera/util/PuzzleMerge.kt
Normal file
59
app/src/main/java/com/inspection/camera/util/PuzzleMerge.kt
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package com.inspection.camera.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
object PuzzleMerge {
|
||||||
|
// Merge up to 4 images into a single 2x2 bitmap
|
||||||
|
fun mergeToBitmap(context: Context, imageUris: List<Uri>, targetSize: Int = 1000): Bitmap? {
|
||||||
|
if (imageUris.isEmpty()) return null
|
||||||
|
val size = targetSize
|
||||||
|
val half = size / 2
|
||||||
|
val merged = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(merged)
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
|
val targets = arrayOf(
|
||||||
|
Rect(0, 0, half, half), // TL
|
||||||
|
Rect(half, 0, size, half), // TR
|
||||||
|
Rect(0, half, half, size), // BL
|
||||||
|
Rect(half, half, size, size) // BR
|
||||||
|
)
|
||||||
|
|
||||||
|
val toUse = imageUris.take(4)
|
||||||
|
for (i in toUse.indices) {
|
||||||
|
val bmp = loadBitmap(context, toUse[i], half, half)
|
||||||
|
if (bmp != null) {
|
||||||
|
val scaled = Bitmap.createScaledBitmap(bmp, half, half, true)
|
||||||
|
canvas.drawBitmap(scaled, null, targets[i], paint)
|
||||||
|
bmp.recycle()
|
||||||
|
scaled.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: fill empty cells with placeholder color if needed
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load bitmap from URI with sampling to fit within maxW x maxH
|
||||||
|
private fun loadBitmap(context: Context, uri: Uri, maxW: Int, maxH: Int): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
|
context.contentResolver.openInputStream(uri).use { ins ->
|
||||||
|
BitmapFactory.decodeStream(ins, null, opts)
|
||||||
|
}
|
||||||
|
val inSample = maxOf(1, max(opts.outWidth / maxW, opts.outHeight / maxH))
|
||||||
|
val opts2 = BitmapFactory.Options().apply { inSampleSize = inSample }
|
||||||
|
context.contentResolver.openInputStream(uri).use { ins ->
|
||||||
|
BitmapFactory.decodeStream(ins, null, opts2)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
test/airtest/test_puzzle_merge.py
Normal file
20
test/airtest/test_puzzle_merge.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
AirTest Script: Puzzle Merge - 2x2 large image composition
|
||||||
|
"""
|
||||||
|
|
||||||
|
from airtest.core.api import *
|
||||||
|
auto_setup(__file__)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
start_app("com.inspection.camera")
|
||||||
|
sleep(2)
|
||||||
|
width, height = device().get_current_resolution()
|
||||||
|
# 进入拼图页入口(假设在屏幕右侧)
|
||||||
|
touch((width * 2 / 3, height - 150))
|
||||||
|
sleep(2)
|
||||||
|
snapshot("puzzle_merge_page.png")
|
||||||
|
print("Saved puzzle_merge_page.png")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
116
test_android.py
116
test_android.py
@@ -1,116 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Android 模拟器图像识别测试脚本
|
|
||||||
"""
|
|
||||||
from airtest.core.api import *
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 初始化设备连接
|
|
||||||
def init_device():
|
|
||||||
# 连接 Android 模拟器
|
|
||||||
dev = connect_device("android://127.0.0.1:5037/emulator-5554")
|
|
||||||
print(f"已连接设备: {dev}")
|
|
||||||
return dev
|
|
||||||
|
|
||||||
# 截取当前屏幕
|
|
||||||
def capture_screen(filename="screen.png"):
|
|
||||||
dev = device()
|
|
||||||
screen = dev.snapshot()
|
|
||||||
|
|
||||||
# numpy.ndarray 转换为 PIL Image 并保存
|
|
||||||
from PIL import Image
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
img = Image.fromarray(screen)
|
|
||||||
img.save(filename)
|
|
||||||
print(f"截图已保存: {filename}")
|
|
||||||
return screen
|
|
||||||
|
|
||||||
# 测试1: 打开相机应用
|
|
||||||
def test_open_camera():
|
|
||||||
print("测试1: 打开相机应用...")
|
|
||||||
|
|
||||||
# 使用 ADB 启动相机
|
|
||||||
shell("am start -n com.inspection.camera/.ui.MainActivity")
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
# 截图
|
|
||||||
capture_screen("test_open_camera.png")
|
|
||||||
print("相机已打开")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 测试2: 模拟点击屏幕中心(测试触摸功能)
|
|
||||||
def test_touch():
|
|
||||||
print("测试2: 测试触摸功能...")
|
|
||||||
|
|
||||||
# 点击屏幕中心
|
|
||||||
touch([540, 960]) # 模拟器分辨率 1080x1920 的中心点
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
capture_screen("test_touch.png")
|
|
||||||
print("触摸测试完成")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 测试3: 滑动测试
|
|
||||||
def test_swipe():
|
|
||||||
print("测试3: 测试滑动功能...")
|
|
||||||
|
|
||||||
# 从上往下滑动
|
|
||||||
swipe([540, 300], [540, 900])
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
capture_screen("test_swipe.png")
|
|
||||||
print("滑动测试完成")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 测试4: 查找屏幕上是否有特定文字(使用OCR)
|
|
||||||
def test_ocr():
|
|
||||||
print("测试4: OCR文字识别测试...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from airtest.aircv import aircv
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# 截图
|
|
||||||
screen = device().snapshot()
|
|
||||||
|
|
||||||
# 使用 PIL 显示图像信息
|
|
||||||
from PIL import Image
|
|
||||||
img = Image.fromarray(screen)
|
|
||||||
print(f"屏幕分辨率: {img.size}")
|
|
||||||
print(f"屏幕模式: {img.mode}")
|
|
||||||
|
|
||||||
capture_screen("test_ocr.png")
|
|
||||||
print("OCR 测试完成")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"OCR测试出错: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 主测试函数
|
|
||||||
def main():
|
|
||||||
print("=" * 50)
|
|
||||||
print("Android 模拟器图像识别测试")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 初始化设备
|
|
||||||
init_device()
|
|
||||||
|
|
||||||
# 执行测试
|
|
||||||
test_open_camera()
|
|
||||||
test_touch()
|
|
||||||
test_swipe()
|
|
||||||
test_ocr()
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("所有测试完成!")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"测试出错: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
116
test_camera.py
116
test_camera.py
@@ -1,116 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import sys
|
|
||||||
import io
|
|
||||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
||||||
|
|
||||||
"""
|
|
||||||
测试拍照功能
|
|
||||||
"""
|
|
||||||
from airtest.core.api import *
|
|
||||||
from PIL import Image
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
def init_device():
|
|
||||||
# dev = connect_device("android://127.0.0.1:5037/emulator-5554") # 模拟器
|
|
||||||
dev = connect_device("android://127.0.0.1:5037/APH0219A29002701") # 真机
|
|
||||||
print(f"已连接设备: {dev}")
|
|
||||||
return dev
|
|
||||||
|
|
||||||
def capture_screen(filename="screen.png"):
|
|
||||||
screen = device().snapshot()
|
|
||||||
img = Image.fromarray(screen)
|
|
||||||
img.save(filename)
|
|
||||||
print(f"截图已保存: {filename}")
|
|
||||||
return screen
|
|
||||||
|
|
||||||
def test_camera():
|
|
||||||
print("=" * 50)
|
|
||||||
print("测试:拍照功能")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# 启动相机应用
|
|
||||||
print("1. 启动相机应用...")
|
|
||||||
shell("am start -n com.inspection.camera/.ui.MainActivity")
|
|
||||||
sleep(3)
|
|
||||||
|
|
||||||
capture_screen("01_app_loaded.png")
|
|
||||||
print(" 相机应用已启动")
|
|
||||||
|
|
||||||
# 点击拍照按钮 (屏幕底部中央 - 2400高,底部区域约在2200)
|
|
||||||
print("2. 点击拍照按钮...")
|
|
||||||
touch([540, 2200]) # 拍照按钮位置
|
|
||||||
sleep(3)
|
|
||||||
|
|
||||||
capture_screen("02_after_capture.png")
|
|
||||||
print(" 拍照完成")
|
|
||||||
|
|
||||||
# 检查照片是否生成
|
|
||||||
print("3. 检查照片是否生成...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 创建Pictures目录(如果不存在)
|
|
||||||
shell("mkdir -p /storage/emulated/0/Pictures/inspection")
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# 列出Pictures目录下的文件
|
|
||||||
result = shell("ls -la /storage/emulated/0/Pictures/inspection/")
|
|
||||||
print(f" Pictures目录内容: {result}")
|
|
||||||
|
|
||||||
# 检查是否有新的照片文件
|
|
||||||
files_output = shell("ls /storage/emulated/0/Pictures/inspection/").strip()
|
|
||||||
|
|
||||||
if files_output and "No such file" not in files_output:
|
|
||||||
files = files_output.split('\n')
|
|
||||||
print(f" 照片文件列表: {files}")
|
|
||||||
|
|
||||||
if files and files[0]:
|
|
||||||
latest_photo = files[-1].strip()
|
|
||||||
print(f" [OK] 照片已生成: {latest_photo}")
|
|
||||||
|
|
||||||
# 拉取最新照片到本地
|
|
||||||
pull_cmd = f"/storage/emulated/0/Pictures/inspection/{latest_photo}"
|
|
||||||
os.system(f'"C:\\Users\\xiaji\\AppData\\Local\\Android\\Sdk\\platform-tools\\adb.exe" -s emulator-5554 pull "{pull_cmd}" . 2>nul')
|
|
||||||
|
|
||||||
if os.path.exists(latest_photo):
|
|
||||||
print(f" [OK] 照片已保存到本地: {latest_photo}")
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("测试结果: 拍照功能正常!")
|
|
||||||
print("=" * 50)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(" [X] 照片未保存到本地")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print(" [X] 未找到照片文件")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# 检查DCIM目录
|
|
||||||
dcim_result = shell("ls /storage/emulated/0/DCIM/Camera/")
|
|
||||||
print(f" DCIM目录内容: {dcim_result}")
|
|
||||||
if dcim_result and "No such file" not in dcim_result and dcim_result.strip():
|
|
||||||
print(f" [OK] 照片已生成在DCIM目录")
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("测试结果: 拍照功能正常!")
|
|
||||||
print("=" * 50)
|
|
||||||
return True
|
|
||||||
|
|
||||||
print(" [X] 未找到任何照片")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" [X] 检查照片时出错: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
init_device()
|
|
||||||
result = test_camera()
|
|
||||||
if result:
|
|
||||||
print("\n测试通过!")
|
|
||||||
else:
|
|
||||||
print("\n测试失败!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"测试出错: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Gallery Test - Test if clicking thumbnail opens full image
|
|
||||||
"""
|
|
||||||
from airtest.core.api import *
|
|
||||||
from PIL import Image
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def init_device():
|
|
||||||
dev = connect_device("android://127.0.0.1:5037/emulator-5554")
|
|
||||||
print(f"Device connected")
|
|
||||||
return dev
|
|
||||||
|
|
||||||
def capture_screen(filename="screen.png"):
|
|
||||||
screen = device().snapshot()
|
|
||||||
img = Image.fromarray(screen)
|
|
||||||
img.save(filename)
|
|
||||||
print(f"Screenshot: {filename}")
|
|
||||||
return screen
|
|
||||||
|
|
||||||
# Main
|
|
||||||
print("=" * 50)
|
|
||||||
print("Gallery Test - Click Thumbnail")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
init_device()
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
# Go to gallery tab
|
|
||||||
touch([270, 2300]) # Gallery tab
|
|
||||||
sleep(3)
|
|
||||||
capture_screen("gallery1_main.png")
|
|
||||||
|
|
||||||
# Click on first thumbnail (latest image)
|
|
||||||
touch([540, 600])
|
|
||||||
sleep(2)
|
|
||||||
capture_screen("gallery2_after_click.png")
|
|
||||||
|
|
||||||
# Check if screen changed (full image viewer opened)
|
|
||||||
# Compare two screenshots
|
|
||||||
img1 = np.array(Image.open("gallery1_main.png"))
|
|
||||||
img2 = np.array(Image.open("gallery2_after_click.png"))
|
|
||||||
|
|
||||||
# Calculate similarity
|
|
||||||
diff = np.abs(img1.astype(float) - img2.astype(float)).mean()
|
|
||||||
print(f"Screen difference: {diff:.2f}")
|
|
||||||
|
|
||||||
# Check if there's any UI change
|
|
||||||
# If diff > 50, it means screen changed significantly
|
|
||||||
if diff > 50:
|
|
||||||
print("RESULT: Screen changed after clicking thumbnail")
|
|
||||||
print(" (Full image viewer may have opened)")
|
|
||||||
else:
|
|
||||||
print("RESULT: Screen did NOT change after clicking thumbnail")
|
|
||||||
print(" (Click is NOT working - BUG!)")
|
|
||||||
|
|
||||||
print("=" * 50)
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Gallery Test - Test clicking thumbnail to view full image
|
|
||||||
"""
|
|
||||||
from airtest.core.api import *
|
|
||||||
from PIL import Image
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def init_device():
|
|
||||||
dev = connect_device("android://127.0.0.1:5037/emulator-5554")
|
|
||||||
print(f"Device connected")
|
|
||||||
return dev
|
|
||||||
|
|
||||||
def capture_screen(filename="screen.png"):
|
|
||||||
screen = device().snapshot()
|
|
||||||
img = Image.fromarray(screen)
|
|
||||||
img.save(filename)
|
|
||||||
print(f"Screenshot: {filename}")
|
|
||||||
return screen
|
|
||||||
|
|
||||||
def calculate_similarity(img1_path, img2_path):
|
|
||||||
img1 = np.array(Image.open(img1_path))
|
|
||||||
img2 = np.array(Image.open(img2_path))
|
|
||||||
diff = np.abs(img1.astype(float) - img2.astype(float)).mean()
|
|
||||||
return diff
|
|
||||||
|
|
||||||
print("=" * 50)
|
|
||||||
print("Gallery Test - Click Thumbnail to View Full Image")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
init_device()
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
# Step 1: Open Gallery app
|
|
||||||
print("Step 1: Open Gallery app")
|
|
||||||
touch([270, 2300]) # Gallery tab
|
|
||||||
sleep(3)
|
|
||||||
capture_screen("gallery_home.png")
|
|
||||||
|
|
||||||
# Step 2: Verify thumbnails are displayed
|
|
||||||
print("Step 2: Verify thumbnails are displayed")
|
|
||||||
thumbnails_visible = exists(Template(r"tpl_thumbnail.png"))
|
|
||||||
print(f"Thumbnails visible: {thumbnails_visible}")
|
|
||||||
|
|
||||||
# Step 3: Click on a thumbnail to view full image
|
|
||||||
print("Step 3: Click on first thumbnail")
|
|
||||||
touch([540, 600]) # First thumbnail position
|
|
||||||
sleep(2)
|
|
||||||
capture_screen("full_image.png")
|
|
||||||
|
|
||||||
# Step 4: Verify full image viewer opened
|
|
||||||
print("Step 4: Verify full image viewer opened")
|
|
||||||
diff = calculate_similarity("gallery_home.png", "full_image.png")
|
|
||||||
print(f"Screen difference: {diff:.2f}")
|
|
||||||
|
|
||||||
# Step 5: Test result
|
|
||||||
print("=" * 50)
|
|
||||||
if diff > 50:
|
|
||||||
print("PASS: Clicking thumbnail opens full image viewer")
|
|
||||||
else:
|
|
||||||
print("FAIL: Clicking thumbnail does NOT open full image")
|
|
||||||
print(" (This is a BUG - thumbnail click is not working)")
|
|
||||||
print("=" * 50)
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from airtest.core.api import *
|
|
||||||
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
|
|
||||||
import time
|
|
||||||
|
|
||||||
# 初始化设备
|
|
||||||
init_device("Android", uuid="emulator-5554")
|
|
||||||
poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)
|
|
||||||
|
|
||||||
print("=" * 50)
|
|
||||||
print("开始测试拍照功能 - 位置水印测试")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
print("\n[1] 启动应用...")
|
|
||||||
home()
|
|
||||||
start_app("com.inspection.camera")
|
|
||||||
sleep(5)
|
|
||||||
|
|
||||||
# 等待应用加载
|
|
||||||
print("[2] 等待应用加载...")
|
|
||||||
snapshot("app_loaded.png")
|
|
||||||
print("[INFO] 已保存截图: app_loaded.png")
|
|
||||||
|
|
||||||
# 等待定位完成,检测定位文本
|
|
||||||
print("\n[3] 等待定位完成(最多15秒)...")
|
|
||||||
location_text = ""
|
|
||||||
for i in range(15):
|
|
||||||
try:
|
|
||||||
# 尝试通过poco获取定位文本(UI中的卡片文本)
|
|
||||||
# 定位文本可能位于某个Card内,我们尝试查找包含"定位"的文本元素
|
|
||||||
elements = poco(textMatches=".*定位.*")
|
|
||||||
if elements.exists():
|
|
||||||
location_text = elements.get_text()
|
|
||||||
print(f"[INFO] 检测到定位文本: {location_text}")
|
|
||||||
if location_text != "正在定位...":
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
sleep(1)
|
|
||||||
print(f"等待 {i+1}/15 秒")
|
|
||||||
|
|
||||||
# 如果未找到定位文本,尝试其他选择器
|
|
||||||
if not location_text:
|
|
||||||
try:
|
|
||||||
# 尝试查找包含"定位"或"位置"的文本
|
|
||||||
for text in ["定位", "位置", "地点"]:
|
|
||||||
elements = poco(textMatches=f".*{text}.*")
|
|
||||||
if elements.exists():
|
|
||||||
location_text = elements.get_text()
|
|
||||||
print(f"[INFO] 通过关键词 '{text}' 找到定位文本: {location_text}")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[WARNING] 查找定位文本失败: {e}")
|
|
||||||
|
|
||||||
print(f"\n[INFO] 最终定位文本: '{location_text}'")
|
|
||||||
|
|
||||||
# 验证定位是否失败(超时后应显示"定位失败")
|
|
||||||
if location_text == "定位失败":
|
|
||||||
print("[SUCCESS] 定位超时后正确显示'定位失败'")
|
|
||||||
else:
|
|
||||||
print(f"[WARNING] 定位文本不是'定位失败',而是: {location_text}")
|
|
||||||
|
|
||||||
# 点击拍照按钮(屏幕中央下方)
|
|
||||||
print("\n[4] 点击拍照按钮...")
|
|
||||||
shell("input tap 540 2100")
|
|
||||||
sleep(3)
|
|
||||||
snapshot("after_capture.png")
|
|
||||||
print("[INFO] 拍照后截图: after_capture.png")
|
|
||||||
|
|
||||||
# 检查保存的图片文件
|
|
||||||
print("\n[4.5] 检查保存的图片文件...")
|
|
||||||
sleep(2) # 等待文件保存
|
|
||||||
# 列出图片目录
|
|
||||||
output = shell("ls -t /sdcard/Pictures/InspectionCamera/*.jpg 2>/dev/null | head -1")
|
|
||||||
if output and output.strip():
|
|
||||||
latest_image = output.strip()
|
|
||||||
print(f"[INFO] 找到最新图片: {latest_image}")
|
|
||||||
# 获取文件大小
|
|
||||||
size_output = shell(f"du -h {latest_image} 2>/dev/null | cut -f1")
|
|
||||||
if size_output:
|
|
||||||
print(f"[INFO] 文件大小: {size_output.strip()}")
|
|
||||||
else:
|
|
||||||
print("[WARNING] 未找到保存的图片文件,可能路径不同")
|
|
||||||
# 尝试其他可能路径
|
|
||||||
shell("ls -l /sdcard/Pictures/ 2>/dev/null")
|
|
||||||
shell("ls -l /sdcard/DCIM/ 2>/dev/null")
|
|
||||||
|
|
||||||
# 打开相册
|
|
||||||
print("\n[5] 打开相册...")
|
|
||||||
shell("am start -n com.google.android.apps.photos/.home.HomeActivity")
|
|
||||||
sleep(3)
|
|
||||||
snapshot("gallery.png")
|
|
||||||
print("[INFO] 相册截图: gallery.png")
|
|
||||||
|
|
||||||
# 点击最新图片(假设第一张缩略图位于屏幕中央)
|
|
||||||
print("\n[6] 点击最新图片...")
|
|
||||||
touch([540, 600])
|
|
||||||
sleep(2)
|
|
||||||
snapshot("gallery_detail.png")
|
|
||||||
print("[INFO] 图片详情截图: gallery_detail.png")
|
|
||||||
|
|
||||||
print("\n[7] 测试完成,请检查截图文件中的水印")
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("测试完成!")
|
|
||||||
print("=" * 50)
|
|
||||||
print("\n请检查截图文件:")
|
|
||||||
print("1. app_loaded.png - 应用界面")
|
|
||||||
print("2. after_capture.png - 拍照后")
|
|
||||||
print("3. gallery.png - 相册")
|
|
||||||
print("4. gallery_detail.png - 图片详情")
|
|
||||||
print("\n检查照片左下角是否有时间+位置水印")
|
|
||||||
print("=" * 50)
|
|
||||||
227
test_puzzle.py
227
test_puzzle.py
@@ -1,227 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Puzzle/Merge Function Test Script
|
|
||||||
Test flow:
|
|
||||||
1. Click "Puzzle" in bottom navigation
|
|
||||||
2. Select 2x2 layout
|
|
||||||
3. Select 4 images
|
|
||||||
4. Click "Preview"
|
|
||||||
5. Confirm merge result
|
|
||||||
6. Return to main page
|
|
||||||
7. Enter gallery
|
|
||||||
8. Check latest merge result
|
|
||||||
"""
|
|
||||||
from airtest.core.api import *
|
|
||||||
from PIL import Image
|
|
||||||
import numpy as np
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Initialize device
|
|
||||||
def init_device():
|
|
||||||
dev = connect_device("android://127.0.0.1:5037/emulator-5554")
|
|
||||||
print(f"Device connected: {dev}")
|
|
||||||
return dev
|
|
||||||
|
|
||||||
# Capture screen
|
|
||||||
def capture_screen(filename="screen.png"):
|
|
||||||
screen = device().snapshot()
|
|
||||||
img = Image.fromarray(screen)
|
|
||||||
img.save(filename)
|
|
||||||
print(f"Screenshot saved: {filename}")
|
|
||||||
return screen
|
|
||||||
|
|
||||||
# Wait for app launch
|
|
||||||
def wait_app_launch():
|
|
||||||
print("Waiting for app launch...")
|
|
||||||
sleep(3)
|
|
||||||
|
|
||||||
# Test 1: Click "Puzzle" tab in bottom navigation
|
|
||||||
def click_merge_tab():
|
|
||||||
print("Test 1: Click 'Puzzle' tab...")
|
|
||||||
# Bottom navigation: Camera(0), Gallery(1), Puzzle(2), Settings(3)
|
|
||||||
# Click puzzle button at bottom right
|
|
||||||
touch([810, 2300])
|
|
||||||
sleep(2)
|
|
||||||
capture_screen("01_click_merge.png")
|
|
||||||
print("Clicked puzzle")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 2: Select 2x2 layout
|
|
||||||
def select_2x2_layout():
|
|
||||||
print("Test 2: Select 2x2 layout...")
|
|
||||||
touch([180, 200])
|
|
||||||
sleep(1)
|
|
||||||
capture_screen("02_select_2x2.png")
|
|
||||||
print("Selected 2x2 layout")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 3: Select 4 images
|
|
||||||
def select_4_images():
|
|
||||||
print("Test 3: Select 4 images...")
|
|
||||||
|
|
||||||
# Click add image button (first empty cell)
|
|
||||||
touch([270, 500])
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
# This will open system image picker
|
|
||||||
capture_screen("03_before_select_image.png")
|
|
||||||
|
|
||||||
# Click "Gallery" option
|
|
||||||
touch([540, 800])
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# Select first image
|
|
||||||
touch([200, 400])
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# Second image
|
|
||||||
touch([540, 400])
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# Third image
|
|
||||||
touch([880, 400])
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# Fourth image
|
|
||||||
touch([200, 700])
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# Confirm selection (click OK button)
|
|
||||||
touch([900, 2300])
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
capture_screen("04_selected_4_images.png")
|
|
||||||
print("Selected 4 images")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 4: Click preview button
|
|
||||||
def click_preview_button():
|
|
||||||
print("Test 4: Click preview button...")
|
|
||||||
touch([300, 2200])
|
|
||||||
sleep(3)
|
|
||||||
capture_screen("05_preview.png")
|
|
||||||
print("Clicked preview")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 5: Close preview dialog
|
|
||||||
def close_preview():
|
|
||||||
print("Test 5: Close preview dialog...")
|
|
||||||
touch([540, 1500])
|
|
||||||
sleep(1)
|
|
||||||
capture_screen("06_preview_closed.png")
|
|
||||||
print("Closed preview")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 6: Click save button
|
|
||||||
def click_save_button():
|
|
||||||
print("Test 6: Click save button...")
|
|
||||||
touch([800, 2200])
|
|
||||||
sleep(2)
|
|
||||||
capture_screen("07_save_dialog.png")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 7: Confirm save
|
|
||||||
def confirm_save():
|
|
||||||
print("Test 7: Confirm save...")
|
|
||||||
touch([700, 1400])
|
|
||||||
sleep(3)
|
|
||||||
capture_screen("08_saved.png")
|
|
||||||
print("Saved")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 8: Return to home page
|
|
||||||
def go_back_home():
|
|
||||||
print("Test 8: Return to home page...")
|
|
||||||
touch([100, 100])
|
|
||||||
sleep(2)
|
|
||||||
capture_screen("09_back_home.png")
|
|
||||||
print("Returned to home")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 9: Click "Gallery" tab
|
|
||||||
def click_gallery_tab():
|
|
||||||
print("Test 9: Click 'Gallery' tab...")
|
|
||||||
touch([270, 2300])
|
|
||||||
sleep(3)
|
|
||||||
capture_screen("10_gallery.png")
|
|
||||||
print("Clicked gallery")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Test 10: View latest image
|
|
||||||
def view_latest_image():
|
|
||||||
print("Test 10: View latest image...")
|
|
||||||
touch([540, 600])
|
|
||||||
sleep(2)
|
|
||||||
capture_screen("11_latest_image.png")
|
|
||||||
print("Opened latest image")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 测试11: 检查图片是否为纯白色
|
|
||||||
def check_white_image():
|
|
||||||
print("Test 11: Check if image is pure white...")
|
|
||||||
|
|
||||||
# 读取截图
|
|
||||||
img = Image.open("11_latest_image.png")
|
|
||||||
img_array = np.array(img)
|
|
||||||
|
|
||||||
# 检查是否为纯白色 (RGB都接近255)
|
|
||||||
# 取图片中心区域进行判断
|
|
||||||
h, w = img_array.shape[:2]
|
|
||||||
center_region = img_array[h//4:3*h//4, w//4:3*w//4]
|
|
||||||
|
|
||||||
# 计算平均颜色
|
|
||||||
avg_color = np.mean(center_region, axis=(0, 1))
|
|
||||||
print(f"Image center average color: {avg_color}")
|
|
||||||
|
|
||||||
# 判断是否接近白色 (R, G, B 都 > 250)
|
|
||||||
is_white = all(c > 250 for c in avg_color[:3])
|
|
||||||
|
|
||||||
if is_white:
|
|
||||||
print("WARNING: Image is pure white, merge function may not work!")
|
|
||||||
else:
|
|
||||||
print("OK: Image is not pure white, merge function works")
|
|
||||||
|
|
||||||
return not is_white
|
|
||||||
|
|
||||||
# Main test function
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print("Puzzle/Merge Function Automated Test")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize device
|
|
||||||
init_device()
|
|
||||||
|
|
||||||
# Wait for app launch
|
|
||||||
wait_app_launch()
|
|
||||||
|
|
||||||
# Execute test steps
|
|
||||||
click_merge_tab()
|
|
||||||
select_2x2_layout()
|
|
||||||
select_4_images()
|
|
||||||
click_preview_button()
|
|
||||||
close_preview()
|
|
||||||
click_save_button()
|
|
||||||
confirm_save()
|
|
||||||
go_back_home()
|
|
||||||
click_gallery_tab()
|
|
||||||
view_latest_image()
|
|
||||||
|
|
||||||
# Check result
|
|
||||||
result = check_white_image()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
if result:
|
|
||||||
print("TEST PASSED! Merge function works normally")
|
|
||||||
else:
|
|
||||||
print("TEST FAILED! Merge result is pure white")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Test error: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Simple test to verify merge works
|
|
||||||
"""
|
|
||||||
from airtest.core.api import *
|
|
||||||
from PIL import Image
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def init_device():
|
|
||||||
dev = connect_device("android://127.0.0.1:5037/emulator-5554")
|
|
||||||
print(f"Device connected")
|
|
||||||
return dev
|
|
||||||
|
|
||||||
def capture_screen(filename="screen.png"):
|
|
||||||
screen = device().snapshot()
|
|
||||||
img = Image.fromarray(screen)
|
|
||||||
img.save(filename)
|
|
||||||
print(f"Screenshot: {filename}")
|
|
||||||
return screen
|
|
||||||
|
|
||||||
# Main
|
|
||||||
print("=" * 50)
|
|
||||||
print("Simple Merge Test")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
init_device()
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
# Go to merge screen
|
|
||||||
touch([810, 2300]) # Puzzle tab
|
|
||||||
sleep(2)
|
|
||||||
capture_screen("step1_merge.png")
|
|
||||||
|
|
||||||
# Select 2x2
|
|
||||||
touch([180, 200])
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# Add images
|
|
||||||
touch([270, 500])
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
# Select 4 images from gallery
|
|
||||||
touch([540, 800]) # Gallery tab in picker
|
|
||||||
sleep(1)
|
|
||||||
touch([200, 400]) # Image 1
|
|
||||||
sleep(0.5)
|
|
||||||
touch([540, 400]) # Image 2
|
|
||||||
sleep(0.5)
|
|
||||||
touch([880, 400]) # Image 3
|
|
||||||
sleep(0.5)
|
|
||||||
touch([200, 700]) # Image 4
|
|
||||||
sleep(0.5)
|
|
||||||
touch([900, 2300]) # Confirm
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
capture_screen("step2_selected.png")
|
|
||||||
|
|
||||||
# Click preview
|
|
||||||
touch([300, 2200]) # Preview button
|
|
||||||
sleep(3)
|
|
||||||
|
|
||||||
capture_screen("step3_preview.png")
|
|
||||||
|
|
||||||
# Check preview result
|
|
||||||
img = Image.open("step3_preview.png")
|
|
||||||
img_array = np.array(img)
|
|
||||||
h, w = img_array.shape[:2]
|
|
||||||
center_region = img_array[h//4:3*h//4, w//4:3*w//4]
|
|
||||||
avg_color = np.mean(center_region, axis=(0, 1))
|
|
||||||
print(f"Preview average color: {avg_color}")
|
|
||||||
|
|
||||||
# Close preview
|
|
||||||
touch([540, 1500])
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# Save
|
|
||||||
touch([800, 2200]) # Save button
|
|
||||||
sleep(2)
|
|
||||||
touch([700, 1400]) # Confirm save
|
|
||||||
sleep(3)
|
|
||||||
|
|
||||||
capture_screen("step4_saved.png")
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
if avg_color[0] < 250 and avg_color[1] < 250 and avg_color[2] < 250:
|
|
||||||
print("RESULT: Merge works! Preview shows merged images.")
|
|
||||||
else:
|
|
||||||
print("RESULT: Merge may have issues.")
|
|
||||||
print("=" * 50)
|
|
||||||
Reference in New Issue
Block a user