feat: merge three camera modes into unified CameraActivity

- Added mode toggle button to switch between photo, watermark, and compose modes
- Integrated watermark functionality from WatermarkCameraActivity
- Integrated simple camera functionality from SimpleCameraActivity
- Updated MainActivity to launch CameraActivity directly
- Cleaned up AndroidManifest.xml by removing unused activities
- Updated layout with mode selection button
- All three camera modes now available in single unified interface
This commit is contained in:
xiajiid
2026-02-10 18:42:25 +08:00
parent 547d8a9375
commit e75c9f5c45
9 changed files with 192 additions and 856 deletions

View File

@@ -28,14 +28,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SimpleCameraActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".WatermarkCameraActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".CameraActivity"
android:exported="false"

View File

@@ -35,7 +35,7 @@ import java.util.concurrent.Executors
class CameraActivity : AppCompatActivity() {
companion object {
companion object {
private const val TAG = "LogCam"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(
@@ -43,11 +43,27 @@ class CameraActivity : AppCompatActivity() {
)
}
private lateinit var viewFinder: PreviewView
private lateinit var captureButton: ImageButton
private lateinit var settingsButton: ImageButton
private lateinit var modeButton: Button // 模式切换按钮
private lateinit var photoPreviewLayout: LinearLayout
private var imageCapture: ImageCapture? = null
// 拍照模式相关变量
private var captureMode = MODE_PHOTO // 默认为普通拍照模式
private var watermarkEnabled = true // 是否启用时间戳水印
// 常量定义
companion object {
private const val MODE_PHOTO = 0 // 普通拍照
private const val MODE_WATERMARK = 1 // 水印模式
private const val MODE_COMPOSE = 2 // 合成模式
}
private lateinit var cameraExecutor: ExecutorService
private var cameraProvider: ProcessCameraProvider? = null
@@ -66,6 +82,10 @@ class CameraActivity : AppCompatActivity() {
settingsButton = findViewById(R.id.settingsButton)
photoPreviewLayout = findViewById(R.id.photoPreviewLayout)
// 初始化模式切换按钮
modeButton = findViewById(R.id.modeButton)
modeButton.setOnClickListener { showModeSelectionDialog() }
// 初始化线程池
cameraExecutor = Executors.newSingleThreadExecutor()
@@ -81,6 +101,53 @@ class CameraActivity : AppCompatActivity() {
// 设置点击监听器
captureButton.setOnClickListener { takePhoto() }
settingsButton.setOnClickListener { openSettings() }
private fun showModeSelectionDialog() {
val popupMenu = PopupMenu(this, modeButton)
popupMenu.menu.apply {
add("普通模式").setOnMenuItemClickListener {
setCaptureMode(MODE_PHOTO)
true
}
add("水印模式").setOnMenuItemClickListener {
setCaptureMode(MODE_WATERMARK)
true
}
add("合成模式").setOnMenuItemClickListener {
setCaptureMode(MODE_COMPOSE)
true
}
}
popupMenu.show()
}
private fun setCaptureMode(mode: Int) {
captureMode = mode
when (mode) {
MODE_PHOTO -> {
modeButton.text = "模式: 普通"
watermarkEnabled = false
}
MODE_WATERMARK -> {
modeButton.text = "模式: 水印"
watermarkEnabled = true
}
MODE_COMPOSE -> {
modeButton.text = "模式: 合成"
watermarkEnabled = false
}
}
Toast.makeText(this, "已切换到${getModeName(mode)}", Toast.LENGTH_SHORT).show()
}
private fun getModeName(mode: Int): String {
return when (mode) {
MODE_PHOTO -> "普通模式"
MODE_WATERMARK -> "水印模式"
MODE_COMPOSE -> "合成模式"
else -> "未知模式"
}
}
}
override fun onDestroy() {
@@ -330,6 +397,110 @@ class CameraActivity : AppCompatActivity() {
}
}
)
private fun addWatermarkAndSave(originalUri: Uri) {
try {
// 读取原始图片
val inputStream = contentResolver.openInputStream(originalUri)
val originalBitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
if (originalBitmap == null) {
Log.e(TAG, "无法读取图片: $originalUri")
return
}
// 添加水印
val watermarkedBitmap = addWatermark(originalBitmap)
// 保存带水印的图片
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "巡检_${timestamp}_水印.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/LogCam")
}
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
val outputStream = contentResolver.openOutputStream(uri)
if (outputStream != null) {
watermarkedBitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStream)
outputStream.close()
// 通知相册更新
MediaStore.Images.Media.insertImage(contentResolver, watermarkedBitmap, contentValues.getAsString(MediaStore.MediaColumns.DISPLAY_NAME), "")
runOnUiThread {
Toast.makeText(baseContext, "图片已保存并添加水印", Toast.LENGTH_LONG).show()
addPhotoToPreview(uri)
}
} else {
Log.e(TAG, "无法打开输出流: $uri")
}
} else {
Log.e(TAG, "无法创建新文件")
}
// 回收位图
originalBitmap.recycle()
watermarkedBitmap.recycle()
} catch (e: Exception) {
Log.e(TAG, "添加水印失败: ${e.message}", e)
runOnUiThread {
Toast.makeText(baseContext, "添加水印失败: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
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
isAntiAlias = true
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
textAlign = Paint.Align.RIGHT
}
// 计算文本大小
val bounds = android.graphics.Rect()
paint.getTextBounds(timestamp, 0, timestamp.length, bounds)
// 在右下角绘制时间戳水印
val padding = 50f
canvas.drawText(
timestamp,
mutableBitmap.width - padding,
mutableBitmap.height - padding - bounds.height(),
paint
)
// 绘制白色边框以提高可读性
val borderPaint = Paint().apply {
color = Color.BLACK
strokeWidth = 4f
style = Paint.Style.STROKE
isAntiAlias = true
}
canvas.drawText(
timestamp,
mutableBitmap.width - padding,
mutableBitmap.height - padding - bounds.height(),
borderPaint
)
return mutableBitmap
}
}
private fun createOutputFileOptions(): ImageCapture.OutputFileOptions {

View File

@@ -1,34 +0,0 @@
package com.example.app
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
class CameraSelectorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera_selector)
val btnSimpleCamera = findViewById<Button>(R.id.btnSimpleCamera)
val btnWatermarkCamera = findViewById<Button>(R.id.btnWatermarkCamera)
val btnMultiCamera = findViewById<Button>(R.id.btnMultiCamera)
val btnSettings = findViewById<Button>(R.id.btnSettings)
btnSimpleCamera.setOnClickListener {
startActivity(Intent(this, SimpleCameraActivity::class.java))
}
btnWatermarkCamera.setOnClickListener {
startActivity(Intent(this, WatermarkCameraActivity::class.java))
}
btnMultiCamera.setOnClickListener {
startActivity(Intent(this, CameraActivity::class.java))
}
btnSettings.setOnClickListener {
startActivity(Intent(this, SettingsFragment::class.java))
}
}
}

View File

@@ -2,33 +2,15 @@ package com.example.app
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera_selector)
setContentView(R.layout.activity_main)
val btnSimpleCamera = findViewById<Button>(R.id.btnSimpleCamera)
val btnWatermarkCamera = findViewById<Button>(R.id.btnWatermarkCamera)
val btnMultiCamera = findViewById<Button>(R.id.btnMultiCamera)
val btnSettings = findViewById<Button>(R.id.btnSettings)
btnSimpleCamera.setOnClickListener {
startActivity(Intent(this, SimpleCameraActivity::class.java))
}
btnWatermarkCamera.setOnClickListener {
startActivity(Intent(this, WatermarkCameraActivity::class.java))
}
btnMultiCamera.setOnClickListener {
startActivity(Intent(this, CameraActivity::class.java))
}
btnSettings.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
// 启动综合相机Activity
startActivity(Intent(this, CameraActivity::class.java))
finish() // 关闭MainActivity避免返回键回到空白页面
}
}
}

View File

@@ -1,320 +0,0 @@
package com.example.app
import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
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.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class SimpleCameraActivity : AppCompatActivity() {
companion object {
private const val TAG = "SimpleCamera"
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)
// 记录应用启动信息
Log.d(TAG, "SimpleCameraActivity 启动")
Log.d(TAG, "Android SDK: ${Build.VERSION.SDK_INT}")
Log.d(TAG, "设备型号: ${Build.MODEL}")
// 初始化视图
viewFinder = findViewById(R.id.viewFinder)
captureButton = findViewById(R.id.captureButton)
// 初始化线程池
cameraExecutor = Executors.newSingleThreadExecutor()
// 请求权限
if (allPermissionsGranted()) {
Log.d(TAG, "权限已授予,启动相机")
startCamera()
} else {
Log.d(TAG, "请求相机权限")
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
// 设置拍照按钮点击监听器
captureButton.setOnClickListener {
Log.d(TAG, "拍照按钮被点击")
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) {
val msg = "照片已保存: ${savedUri.lastPathSegment}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
// 验证文件是否真的存在
try {
val cursor = contentResolver.query(savedUri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
Log.d(TAG, "文件验证成功: ${savedUri}")
// 确认文件已保存
runOnUiThread {
Toast.makeText(baseContext, "照片已成功保存", Toast.LENGTH_SHORT).show()
}
} else {
Log.w(TAG, "文件验证失败但onImageSaved被调用: ${savedUri}")
runOnUiThread {
Toast.makeText(baseContext, "照片可能未保存", Toast.LENGTH_SHORT).show()
}
}
cursor?.close()
} catch (e: Exception) {
Log.e(TAG, "文件验证异常: ${e.message}")
runOnUiThread {
Toast.makeText(baseContext, "照片已保存", Toast.LENGTH_SHORT).show()
}
}
} else {
Log.w(TAG, "onImageSaved被调用但savedUri为null")
runOnUiThread {
Toast.makeText(baseContext, "照片保存完成", Toast.LENGTH_SHORT).show()
}
}
}
override fun onError(exception: ImageCaptureException) {
val errorMsg = "拍照失败: ${exception.message}"
Log.e(TAG, errorMsg, exception)
// 检查错误类型
when (exception.imageCaptureError) {
ImageCapture.ERROR_CAMERA_CLOSED -> {
Log.e(TAG, "相机已关闭")
runOnUiThread {
Toast.makeText(baseContext, "相机已关闭", Toast.LENGTH_LONG).show()
}
}
ImageCapture.ERROR_FILE_IO -> {
Log.e(TAG, "文件保存失败")
runOnUiThread {
Toast.makeText(baseContext, "文件保存失败", Toast.LENGTH_LONG).show()
}
}
ImageCapture.ERROR_INVALID_CAMERA -> {
Log.e(TAG, "无效的相机")
runOnUiThread {
Toast.makeText(baseContext, "无效的相机", Toast.LENGTH_LONG).show()
}
}
ImageCapture.ERROR_CAPTURE_FAILED -> {
// 捕获失败,但可能文件已保存
Log.e(TAG, "图片捕获失败,但可能已保存")
// 验证是否有文件被保存
try {
// 查询最近保存的文件
val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC LIMIT 1"
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, null, null, sortOrder
)
if (cursor != null && cursor.moveToFirst()) {
val displayName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))
Log.d(TAG, "图片捕获失败但文件已存在: $displayName")
runOnUiThread {
Toast.makeText(baseContext, "图片已保存: $displayName", Toast.LENGTH_LONG).show()
}
} else {
runOnUiThread {
Toast.makeText(baseContext, "图片捕获失败", Toast.LENGTH_LONG).show()
}
}
cursor?.close()
} catch (e: Exception) {
Log.e(TAG, "检查保存文件时出错: ${e.message}")
runOnUiThread {
Toast.makeText(baseContext, "图片处理出错,但可能已保存", Toast.LENGTH_LONG).show()
}
}
}
ImageCapture.ERROR_UNKNOWN -> {
Log.e(TAG, "未知错误")
// 特殊处理:如果错误消息包含"processing",可能是内部处理问题
if (exception.message?.contains("processing", ignoreCase = true) == true) {
Log.d(TAG, "检测到processing错误但可能图片已保存")
// 尝试查询最新保存的图片
try {
val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATE_ADDED)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC LIMIT 1"
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, null, null, sortOrder
)
if (cursor != null && cursor.moveToFirst()) {
val displayName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))
val dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED))
val currentTime = System.currentTimeMillis() / 1000
// 检查是否在最近几秒内保存了文件
if (Math.abs(currentTime - dateAdded) <= 5) {
Log.d(TAG, "processing错误但图片已保存: $displayName")
runOnUiThread {
Toast.makeText(baseContext, "图片已保存: $displayName", Toast.LENGTH_LONG).show()
}
} else {
runOnUiThread {
Toast.makeText(baseContext, "图片处理错误", Toast.LENGTH_LONG).show()
}
}
} else {
runOnUiThread {
Toast.makeText(baseContext, "图片处理错误", Toast.LENGTH_LONG).show()
}
}
cursor?.close()
} catch (e: Exception) {
Log.e(TAG, "检查保存文件时出错: ${e.message}")
runOnUiThread {
Toast.makeText(baseContext, "图片处理错误", Toast.LENGTH_LONG).show()
}
}
} else {
runOnUiThread {
Toast.makeText(baseContext, "未知错误", Toast.LENGTH_LONG).show()
}
}
}
else -> {
Log.e(TAG, "其他错误: ${exception.imageCaptureError}")
runOnUiThread {
Toast.makeText(baseContext, "拍照失败: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
}
}
}
)
}
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()
}
}
}
}

View File

@@ -1,391 +0,0 @@
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) {
val msg = "照片已保存: ${savedUri.lastPathSegment}"
Log.d(TAG, msg)
// 验证文件是否真的存在
try {
val cursor = contentResolver.query(savedUri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
Log.d(TAG, "文件验证成功: ${savedUri}")
// 添加水印
addWatermarkToImage(savedUri)
runOnUiThread {
Toast.makeText(baseContext, "照片已保存并添加水印", Toast.LENGTH_SHORT).show()
}
} else {
Log.w(TAG, "文件验证失败但onImageSaved被调用: ${savedUri}")
runOnUiThread {
Toast.makeText(baseContext, "照片可能未保存", Toast.LENGTH_SHORT).show()
}
}
cursor?.close()
} catch (e: Exception) {
Log.e(TAG, "文件验证异常: ${e.message}")
// 即使验证失败,也尝试添加水印
addWatermarkToImage(savedUri)
runOnUiThread {
Toast.makeText(baseContext, "照片已保存并添加水印", Toast.LENGTH_SHORT).show()
}
}
} else {
Log.w(TAG, "onImageSaved被调用但savedUri为null")
runOnUiThread {
Toast.makeText(baseContext, "照片保存完成", Toast.LENGTH_SHORT).show()
}
}
}
override fun onError(exception: ImageCaptureException) {
val errorMsg = "拍照失败: ${exception.message}"
Log.e(TAG, errorMsg, exception)
// 检查错误类型
when (exception.imageCaptureError) {
ImageCapture.ERROR_CAMERA_CLOSED -> {
Log.e(TAG, "相机已关闭")
runOnUiThread {
Toast.makeText(baseContext, "相机已关闭", Toast.LENGTH_LONG).show()
}
}
ImageCapture.ERROR_FILE_IO -> {
Log.e(TAG, "文件保存失败")
runOnUiThread {
Toast.makeText(baseContext, "文件保存失败", Toast.LENGTH_LONG).show()
}
}
ImageCapture.ERROR_INVALID_CAMERA -> {
Log.e(TAG, "无效的相机")
runOnUiThread {
Toast.makeText(baseContext, "无效的相机", Toast.LENGTH_LONG).show()
}
}
ImageCapture.ERROR_CAPTURE_FAILED -> {
// 捕获失败,但可能文件已保存
Log.e(TAG, "图片捕获失败,但可能已保存")
// 验证是否有文件被保存
try {
// 查询最近保存的文件
val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC LIMIT 1"
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, null, null, sortOrder
)
if (cursor != null && cursor.moveToFirst()) {
val displayName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))
Log.d(TAG, "图片捕获失败但文件已存在: $displayName")
// 对最新文件添加水印
val latestUri = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID))
val latestUriObj = Uri.parse("${MediaStore.Images.Media.EXTERNAL_CONTENT_URI}/$latestUri")
addWatermarkToImage(latestUriObj)
runOnUiThread {
Toast.makeText(baseContext, "图片已保存并添加水印: $displayName", Toast.LENGTH_LONG).show()
}
} else {
runOnUiThread {
Toast.makeText(baseContext, "图片捕获失败", Toast.LENGTH_LONG).show()
}
}
cursor?.close()
} catch (e: Exception) {
Log.e(TAG, "检查保存文件时出错: ${e.message}")
runOnUiThread {
Toast.makeText(baseContext, "图片处理出错,但可能已保存", Toast.LENGTH_LONG).show()
}
}
}
ImageCapture.ERROR_UNKNOWN -> {
Log.e(TAG, "未知错误")
// 特殊处理:如果错误消息包含"processing",可能是内部处理问题
if (exception.message?.contains("processing", ignoreCase = true) == true) {
Log.d(TAG, "检测到processing错误但可能图片已保存")
// 尝试查询最新保存的图片
try {
val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATE_ADDED)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC LIMIT 1"
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, null, null, sortOrder
)
if (cursor != null && cursor.moveToFirst()) {
val displayName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))
val dateAdded = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED))
val currentTime = System.currentTimeMillis() / 1000
// 检查是否在最近几秒内保存了文件
if (Math.abs(currentTime - dateAdded) <= 5) {
Log.d(TAG, "processing错误但图片已保存: $displayName")
// 对最新文件添加水印
val latestUri = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID))
val latestUriObj = Uri.parse("${MediaStore.Images.Media.EXTERNAL_CONTENT_URI}/$latestUri")
addWatermarkToImage(latestUriObj)
runOnUiThread {
Toast.makeText(baseContext, "图片已保存并添加水印: $displayName", Toast.LENGTH_LONG).show()
}
} else {
runOnUiThread {
Toast.makeText(baseContext, "图片处理错误", Toast.LENGTH_LONG).show()
}
}
} else {
runOnUiThread {
Toast.makeText(baseContext, "图片处理错误", Toast.LENGTH_LONG).show()
}
}
cursor?.close()
} catch (e: Exception) {
Log.e(TAG, "检查保存文件时出错: ${e.message}")
runOnUiThread {
Toast.makeText(baseContext, "图片处理错误", Toast.LENGTH_LONG).show()
}
}
} else {
runOnUiThread {
Toast.makeText(baseContext, "未知错误", Toast.LENGTH_LONG).show()
}
}
}
else -> {
Log.e(TAG, "其他错误: ${exception.imageCaptureError}")
runOnUiThread {
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()
}
}
}
}

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center"
tools:context=".CameraSelectorActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="选择相机模式"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="48dp"
android:textColor="@color/black" />
<Button
android:id="@+id/btnSimpleCamera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="简单相机"
android:textSize="18sp"
android:padding="20dp" />
<Button
android:id="@+id/btnWatermarkCamera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="水印相机"
android:textSize="18sp"
android:padding="20dp" />
<Button
android:id="@+id/btnMultiCamera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="多图合成相机"
android:textSize="18sp"
android:padding="20dp" />
<Button
android:id="@+id/btnSettings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="设置"
android:textSize="16sp"
android:padding="16dp" />
</LinearLayout>

View File

@@ -5,7 +5,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
tools:context=".CameraActivity">
<!-- CameraView for displaying the camera preview -->
<androidx.camera.view.PreviewView
@@ -35,6 +35,16 @@
android:orientation="horizontal"
android:gravity="center">
<!-- Mode button -->
<ImageButton
android:id="@+id/modeButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_camera"
android:contentDescription="Change mode" />
<!-- Capture button -->
<ImageButton
android:id="@+id/captureButton"

View File

@@ -6,7 +6,6 @@
android:layout_height="match_parent"
tools:context=".SimpleCameraActivity">
<!-- 相机预览视图 -->
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="0dp"
@@ -16,31 +15,14 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 拍照按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/captureButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:src="@android:drawable/ic_menu_camera"
app:backgroundTint="@color/primary"
android:layout_margin="24dp"
android:src="@drawable/ic_camera"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:tint="@android:color/white" />
<!-- 状态提示 -->
<TextView
android:id="@+id/statusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="极简相机 - 点击下方按钮拍照"
android:textColor="@android:color/white"
android:textSize="14sp"
android:background="#80000000"
android:padding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>