Add WatermarkCameraActivity with timestamp watermark and development plan

This commit is contained in:
xiajiid
2026-02-08 22:20:32 +08:00
parent b87d90e418
commit f1a7811d28
5 changed files with 362 additions and 4 deletions

102
DEVELOPMENT_PLAN.md Normal file
View File

@@ -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中。

View File

@@ -30,6 +30,10 @@
android:name=".SimpleCameraActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".WatermarkCameraActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application>
</manifest>

View File

@@ -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避免返回键回到空白页面
}
}

View File

@@ -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)
}

View File

@@ -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<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this, "相机权限被拒绝,应用无法使用相机功能", Toast.LENGTH_LONG).show()
finish()
}
}
}
}