From f1a7811d28b01df77c92096dbbb2cf16f0e66041 Mon Sep 17 00:00:00 2001 From: xiajiid Date: Sun, 8 Feb 2026 22:20:32 +0800 Subject: [PATCH] Add WatermarkCameraActivity with timestamp watermark and development plan --- DEVELOPMENT_PLAN.md | 102 +++++++ app/src/main/AndroidManifest.xml | 4 + .../main/java/com/example/app/MainActivity.kt | 4 +- .../com/example/app/SimpleCameraActivity.kt | 8 +- .../example/app/WatermarkCameraActivity.kt | 248 ++++++++++++++++++ 5 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 DEVELOPMENT_PLAN.md create mode 100644 app/src/main/java/com/example/app/WatermarkCameraActivity.kt 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