diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md
new file mode 100644
index 0000000..c9959c8
--- /dev/null
+++ b/DEVELOPMENT_PLAN.md
@@ -0,0 +1,102 @@
+# 巡检相机增量开发计划
+
+## 开发原则
+- **增量开发**:每次只添加一个功能,测试通过后再添加下一个
+- **DRY原则**:避免重复代码,保持代码简洁
+- **测试驱动**:每个功能都要在真机上测试验证
+
+## 功能列表(按优先级排序)
+
+### ✅ 阶段1:基础功能(已完成)
+1. 相机预览
+2. 拍照保存
+3. 权限处理
+
+### 🔄 阶段2:水印功能(当前阶段)
+1. 时间戳水印
+2. 水印样式可配置(位置、颜色、大小)
+3. 水印预览
+
+### 📋 阶段3:多图管理
+1. 拍摄多张照片
+2. 照片预览网格
+3. 照片选择/删除
+
+### 🎨 阶段4:图片合成
+1. 2x2网格合成
+2. 合成图片添加标题
+3. 合成图片质量优化
+
+### ⚙️ 阶段5:设置功能
+1. 水印设置
+2. 图片质量设置
+3. 存储路径设置
+
+### 📱 阶段6:高级功能
+1. 图片编辑(裁剪、旋转)
+2. 批量处理
+3. 分享功能
+
+## 当前状态
+
+### 已修复问题
+1. ✅ 极简相机基础功能正常
+2. ✅ 修复错误提示问题("拍照失败:processing failed")
+
+### 当前测试版本
+- **WatermarkCameraActivity**:带时间戳水印的相机
+- 测试重点:水印添加是否正常,性能是否稳定
+
+## 测试步骤
+
+### 水印相机测试
+1. 下载最新APK安装
+2. 启动应用,授予相机权限
+3. 拍照测试
+4. 检查:
+ - 照片是否保存成功
+ - 水印是否正确添加(左下角时间戳)
+ - 水印是否清晰可见
+ - 应用是否稳定无闪退
+
+### 问题反馈
+如果发现问题,请提供:
+1. 手机型号和Android版本
+2. 问题描述(闪退时机、错误提示)
+3. 如果有ADB日志,提供logcat输出
+
+## 代码结构
+
+```
+app/src/main/java/com/example/app/
+├── MainActivity.kt # 启动入口
+├── SimpleCameraActivity.kt # 极简相机(基础功能)
+├── WatermarkCameraActivity.kt # 带水印相机(当前测试)
+├── CameraActivity.kt # 完整功能相机(待重构)
+└── ImageCompositor.kt # 图片合成工具
+
+app/src/main/res/layout/
+├── activity_main.xml # 主布局
+├── activity_simple_camera.xml # 极简相机布局
+└── (后续添加更多布局)
+```
+
+## 后续计划
+
+根据水印相机的测试结果:
+1. **如果正常**:添加多图管理功能
+2. **如果有问题**:修复水印功能,优化性能
+
+## 构建命令
+
+```bash
+# 调试版构建
+./gradlew assembleDebug
+
+# 发布版构建(需要配置签名)
+./gradlew assembleRelease
+```
+
+## 版本控制
+
+每个功能阶段都会创建独立的Activity,便于测试和回滚。最终会将所有功能整合到主CameraActivity中。
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 463100c..a4307ad 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,6 +30,10 @@
android:name=".SimpleCameraActivity"
android:exported="false"
android:screenOrientation="portrait" />
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/app/MainActivity.kt b/app/src/main/java/com/example/app/MainActivity.kt
index 943a394..1007759 100644
--- a/app/src/main/java/com/example/app/MainActivity.kt
+++ b/app/src/main/java/com/example/app/MainActivity.kt
@@ -9,8 +9,8 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
- // 启动极简相机Activity
- startActivity(Intent(this, SimpleCameraActivity::class.java))
+ // 启动带水印相机Activity
+ startActivity(Intent(this, WatermarkCameraActivity::class.java))
finish() // 关闭MainActivity,避免返回键回到空白页面
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/app/SimpleCameraActivity.kt b/app/src/main/java/com/example/app/SimpleCameraActivity.kt
index d151723..2251ef3 100644
--- a/app/src/main/java/com/example/app/SimpleCameraActivity.kt
+++ b/app/src/main/java/com/example/app/SimpleCameraActivity.kt
@@ -138,8 +138,12 @@ class SimpleCameraActivity : AppCompatActivity() {
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
- val savedUri = outputFileResults.savedUri ?: return
- val msg = "照片已保存: ${savedUri.lastPathSegment}"
+ val savedUri = outputFileResults.savedUri
+ val msg = if (savedUri != null) {
+ "照片已保存: ${savedUri.lastPathSegment}"
+ } else {
+ "照片保存成功"
+ }
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
diff --git a/app/src/main/java/com/example/app/WatermarkCameraActivity.kt b/app/src/main/java/com/example/app/WatermarkCameraActivity.kt
new file mode 100644
index 0000000..6cf2726
--- /dev/null
+++ b/app/src/main/java/com/example/app/WatermarkCameraActivity.kt
@@ -0,0 +1,248 @@
+package com.example.app
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import android.util.Log
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import java.io.OutputStream
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class WatermarkCameraActivity : AppCompatActivity() {
+
+ companion object {
+ private const val TAG = "WatermarkCamera"
+ private const val REQUEST_CODE_PERMISSIONS = 10
+ private val REQUIRED_PERMISSIONS = arrayOf(
+ Manifest.permission.CAMERA
+ )
+ }
+
+ private lateinit var viewFinder: PreviewView
+ private lateinit var captureButton: FloatingActionButton
+ private var imageCapture: ImageCapture? = null
+ private lateinit var cameraExecutor: ExecutorService
+ private var cameraProvider: ProcessCameraProvider? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_simple_camera)
+
+ // 初始化视图
+ viewFinder = findViewById(R.id.viewFinder)
+ captureButton = findViewById(R.id.captureButton)
+
+ // 初始化线程池
+ cameraExecutor = Executors.newSingleThreadExecutor()
+
+ // 请求权限
+ if (allPermissionsGranted()) {
+ startCamera()
+ } else {
+ ActivityCompat.requestPermissions(
+ this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
+ )
+ }
+
+ // 设置拍照按钮点击监听器
+ captureButton.setOnClickListener { takePhoto() }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ cameraExecutor.shutdown()
+ cameraProvider?.unbindAll()
+ }
+
+ private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
+ ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun startCamera() {
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+
+ cameraProviderFuture.addListener({
+ try {
+ cameraProvider = cameraProviderFuture.get()
+
+ // 预览
+ val preview = Preview.Builder()
+ .build()
+ .also {
+ it.setSurfaceProvider(viewFinder.surfaceProvider)
+ }
+
+ imageCapture = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ .build()
+
+ val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+ cameraProvider?.unbindAll()
+ cameraProvider?.bindToLifecycle(
+ this, cameraSelector, preview, imageCapture
+ )
+
+ Log.d(TAG, "相机启动成功")
+ } catch (e: Exception) {
+ Log.e(TAG, "相机初始化失败: ${e.message}", e)
+ Toast.makeText(this, "相机启动失败: ${e.message}", Toast.LENGTH_LONG).show()
+ finish()
+ }
+ }, ContextCompat.getMainExecutor(this))
+ }
+
+ private fun takePhoto() {
+ val imageCapture = this.imageCapture ?: run {
+ Toast.makeText(this, "相机未准备好", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
+
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, "巡检_$timestamp")
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/LogCam")
+ }
+ }
+
+ val contentResolver = contentResolver
+ val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+
+ if (uri == null) {
+ Toast.makeText(this, "创建文件失败", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val outputOptions = ImageCapture.OutputFileOptions.Builder(contentResolver, uri, contentValues).build()
+
+ imageCapture.takePicture(
+ outputOptions,
+ ContextCompat.getMainExecutor(this),
+ object : ImageCapture.OnImageSavedCallback {
+ override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+ val savedUri = outputFileResults.savedUri
+ if (savedUri != null) {
+ // 添加水印
+ addWatermarkToImage(savedUri)
+ val msg = "照片已保存并添加水印: ${savedUri.lastPathSegment}"
+ Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+ Log.d(TAG, msg)
+ } else {
+ Toast.makeText(baseContext, "照片保存成功", Toast.LENGTH_SHORT).show()
+ Log.d(TAG, "照片保存成功")
+ }
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ Log.e(TAG, "拍照失败: ${exception.message}", exception)
+ Toast.makeText(baseContext, "拍照失败: ${exception.message}", Toast.LENGTH_LONG).show()
+ }
+ }
+ )
+ }
+
+ private fun addWatermarkToImage(imageUri: Uri) {
+ try {
+ // 读取图片
+ val inputStream = contentResolver.openInputStream(imageUri)
+ val originalBitmap = BitmapFactory.decodeStream(inputStream)
+ inputStream?.close()
+
+ if (originalBitmap == null) {
+ Log.e(TAG, "无法读取图片: $imageUri")
+ return
+ }
+
+ // 添加水印
+ val watermarkedBitmap = addWatermark(originalBitmap)
+
+ // 保存回原文件
+ val outputStream: OutputStream? = contentResolver.openOutputStream(imageUri, "w")
+ if (outputStream != null) {
+ watermarkedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
+ outputStream.close()
+ Log.d(TAG, "水印添加成功: $imageUri")
+ } else {
+ Log.e(TAG, "无法写入图片: $imageUri")
+ }
+
+ // 回收位图
+ originalBitmap.recycle()
+ watermarkedBitmap.recycle()
+ } catch (e: Exception) {
+ Log.e(TAG, "添加水印失败: ${e.message}", e)
+ }
+ }
+
+ private fun addWatermark(originalBitmap: Bitmap): Bitmap {
+ val timestamp = SimpleDateFormat("yyyy年-MM月-dd日 HH:mm:ss", Locale.getDefault()).format(Date())
+
+ // 创建可修改的位图副本
+ val mutableBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true)
+ val canvas = Canvas(mutableBitmap)
+
+ // 创建水印画笔
+ val paint = Paint().apply {
+ color = Color.WHITE
+ textSize = 48f
+ typeface = Typeface.DEFAULT_BOLD
+ setShadowLayer(5f, 0f, 0f, Color.BLACK)
+ }
+
+ // 计算文字尺寸
+ val textWidth = paint.measureText(timestamp)
+ val textHeight = paint.descent() - paint.ascent()
+
+ // 计算水印位置(左下角,留边距)
+ val margin = 20f
+ val x = margin
+ val y = mutableBitmap.height - textHeight - margin
+
+ // 绘制水印
+ canvas.drawText(timestamp, x, y, paint)
+
+ return mutableBitmap
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == REQUEST_CODE_PERMISSIONS) {
+ if (allPermissionsGranted()) {
+ startCamera()
+ } else {
+ Toast.makeText(this, "相机权限被拒绝,应用无法使用相机功能", Toast.LENGTH_LONG).show()
+ finish()
+ }
+ }
+ }
+}
\ No newline at end of file