From ad950e846844a0a9174cf5c897fb4ea58c59bf73 Mon Sep 17 00:00:00 2001 From: xiajiid Date: Sun, 8 Feb 2026 12:16:31 +0800 Subject: [PATCH] Add image composition and text overlay functionality --- .../java/com/example/app/CameraActivity.kt | 159 +++++++++++++++++- .../java/com/example/app/ImageCompositor.kt | 145 ++++++++++++++++ 2 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/example/app/ImageCompositor.kt diff --git a/app/src/main/java/com/example/app/CameraActivity.kt b/app/src/main/java/com/example/app/CameraActivity.kt index ea684e6..3ff04ce 100644 --- a/app/src/main/java/com/example/app/CameraActivity.kt +++ b/app/src/main/java/com/example/app/CameraActivity.kt @@ -18,9 +18,7 @@ import android.os.Bundle import android.os.Looper import android.provider.MediaStore import android.util.Log -import android.widget.ImageButton -import android.widget.LinearLayout -import android.widget.Toast +import android.widget.* import androidx.appcompat.app.AppCompatActivity import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture @@ -31,11 +29,11 @@ import androidx.camera.view.PreviewView import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.google.android.material.snackbar.Snackbar +import java.io.ByteArrayOutputStream import java.io.IOException import java.io.OutputStream import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale +import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import kotlin.math.roundToInt @@ -57,6 +55,11 @@ class CameraActivity : AppCompatActivity() { private lateinit var photoPreviewLayout: LinearLayout private var imageCapture: ImageCapture? = null private lateinit var cameraExecutor: ExecutorService + + // 存储拍摄的图片URI + private val capturedImageUris = mutableListOf() + // 图片合成器 + private val compositor = ImageCompositor() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -140,10 +143,18 @@ class CameraActivity : AppCompatActivity() { } override fun onImageSaved(output: ImageCapture.OutputFileResults) { - // 添加水印到已保存的照片 + // 获取保存的URI val savedUri = output.savedUri ?: return + + // 添加水印到已保存的照片 addWatermarkToImage(savedUri) + // 添加到捕获的图片列表 + capturedImageUris.add(savedUri) + + // 更新预览 + updatePhotoPreviews() + Toast.makeText( baseContext, "照片已保存", @@ -215,6 +226,142 @@ class CameraActivity : AppCompatActivity() { return watermarkedBitmap } + private fun updatePhotoPreviews() { + // 清空当前预览 + photoPreviewLayout.removeAllViews() + + // 限制最多显示4张预览图 + val displayCount = minOf(capturedImageUris.size, 4) + + for (i in 0 until displayCount) { + val imageView = ImageView(this) + val params = LinearLayout.LayoutParams( + 200, 200 + ).apply { + setMargins(8, 8, 8, 8) + } + imageView.layoutParams = params + imageView.scaleType = ImageView.ScaleType.CENTER_CROP + + // 加载并设置图片 + val uri = capturedImageUris[capturedImageUris.size - displayCount + i] // 显示最新的几张 + imageView.setImageURI(uri) + + // 添加点击事件以查看大图 + imageView.setOnClickListener { + // 可以在这里实现查看大图的功能 + } + + photoPreviewLayout.addView(imageView) + } + + // 如果已有4张图片,添加合成按钮 + if (capturedImageUris.size >= 4) { + addComposeButton() + } + } + + private fun addComposeButton() { + // 添加合成按钮 + val composeButton = Button(this) + composeButton.text = "合成图片" + composeButton.setBackgroundColor(ContextCompat.getColor(this, R.color.purple_200)) + composeButton.setTextColor(Color.WHITE) + + val params = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(8, 8, 8, 8) + gravity = android.view.Gravity.CENTER_HORIZONTAL + } + composeButton.layoutParams = params + + composeButton.setOnClickListener { + composeLastFourImages() + } + + photoPreviewLayout.addView(composeButton) + } + + private fun composeLastFourImages() { + // 获取最近的4张图片 + val recentUris = if (capturedImageUris.size >= 4) { + capturedImageUris.takeLast(4) + } else { + capturedImageUris + } + + // 将URI转换为Bitmap + val bitmaps = mutableListOf() + for (uri in recentUris) { + try { + val inputStream = contentResolver.openInputStream(uri) + val bitmap = BitmapFactory.decodeStream(inputStream) + inputStream?.close() + bitmaps.add(bitmap) + } catch (e: Exception) { + Log.e(TAG, "加载图片失败: ${e.message}") + } + } + + if (bitmaps.isEmpty()) { + Toast.makeText(this, "没有图片可合成", Toast.LENGTH_SHORT).show() + return + } + + // 合成图片 + val composedBitmap = compositor.composeImagesToGrid(bitmaps, 2000, 2000) // 目标尺寸2000x2000 + + // 添加标题和内容 + val finalBitmap = compositor.addTextOverlay( + composedBitmap, + "巡检报告", + "这是巡检内容的详细描述" + ) + + // 保存合成后的图片 + saveCompositedImage(finalBitmap) + + // 回收位图 + for (bitmap in bitmaps) { + if (bitmap != finalBitmap) { + bitmap.recycle() + } + } + if (finalBitmap != composedBitmap) { + composedBitmap.recycle() + } + } + + private fun saveCompositedImage(bitmap: Bitmap) { + try { + val fileName = compositor.generateFileName() + val contentValues = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + } + } + + val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + ?: throw IOException("无法创建图片URI") + + val outputStream = contentResolver.openOutputStream(uri) + bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStream) + outputStream?.close() + + Toast.makeText(this, "合成图片已保存", Toast.LENGTH_SHORT).show() + + // 刷新相册 + refreshGallery(uri) + } catch (e: Exception) { + Log.e(TAG, "保存合成图片失败: ${e.message}") + Toast.makeText(this, "保存合成图片失败", Toast.LENGTH_SHORT).show() + } + } + private fun refreshGallery(uri: Uri) { // 通知相册更新 val contentValues = ContentValues().apply { diff --git a/app/src/main/java/com/example/app/ImageCompositor.kt b/app/src/main/java/com/example/app/ImageCompositor.kt new file mode 100644 index 0000000..9c510f9 --- /dev/null +++ b/app/src/main/java/com/example/app/ImageCompositor.kt @@ -0,0 +1,145 @@ +package com.example.app + +import android.graphics.* +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + +class ImageCompositor { + + /** + * 将多张图片合成到2x2网格布局中 + */ + fun composeImagesToGrid(images: List, targetWidth: Int, targetHeight: Int): Bitmap { + // 创建目标位图 + val composedBitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(composedBitmap) + canvas.drawColor(Color.WHITE) // 白色背景 + + // 计算每个网格的尺寸 + val cellWidth = targetWidth / 2 + val cellHeight = targetHeight / 2 + + // 绘制每张图片到对应网格 + for ((index, bitmap) in images.withIndex()) { + if (index >= 4) break // 最多4张图片 + + val row = index / 2 + val col = index % 2 + + // 计算目标矩形 + val dstRect = Rect(col * cellWidth, row * cellHeight, + (col + 1) * cellWidth, (row + 1) * cellHeight) + + // 缩放图片以适应网格 + val scaledBitmap = Bitmap.createScaledBitmap(bitmap, cellWidth, cellHeight, true) + + // 绘制到画布 + canvas.drawBitmap(scaledBitmap, null, dstRect, null) + + // 回收临时位图 + if (scaledBitmap != bitmap) { + scaledBitmap.recycle() + } + } + + return composedBitmap + } + + /** + * 在图片上添加标题和内容文字 + */ + fun addTextOverlay(bitmap: Bitmap, title: String?, content: String?): Bitmap { + val resultBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true) + val canvas = Canvas(resultBitmap) + + // 文字样式配置 + val titlePaint = Paint().apply { + color = Color.BLACK + textSize = 48f + typeface = Typeface.DEFAULT_BOLD + textAlign = Paint.Align.CENTER + } + + val contentPaint = Paint().apply { + color = Color.BLACK + textSize = 36f + typeface = Typeface.DEFAULT + textAlign = Paint.Align.CENTER + } + + val bgColor = Color.argb(180, 255, 255, 255) // 半透明白色背景 + + // 绘制标题(顶部) + if (!title.isNullOrBlank()) { + // 计算标题背景矩形 + val titleBgHeight = 100 + val titleBgRect = Rect(0, 0, resultBitmap.width, titleBgHeight) + val bgPaint = Paint().apply { color = bgColor } + canvas.drawRect(titleBgRect, bgPaint) + + // 绘制标题文字(居中) + val titleX = resultBitmap.width / 2f + val titleY = titleBgHeight / 2f - (titlePaint.fontMetrics.ascent + titlePaint.fontMetrics.descent) / 2 + canvas.drawText(title, titleX, titleY, titlePaint) + } + + // 绘制内容(底部) + if (!content.isNullOrBlank()) { + // 计算内容背景矩形 + val contentBgHeight = 200 + val contentBgRect = Rect(0, resultBitmap.height - contentBgHeight, + resultBitmap.width, resultBitmap.height) + val bgPaint = Paint().apply { color = bgColor } + canvas.drawRect(contentBgRect, bgPaint) + + // 绘制内容文字(多行处理) + val contentX = resultBitmap.width / 2f + val contentY = resultBitmap.height - contentBgHeight / 2f + drawMultilineText(canvas, content, contentX, contentY, contentPaint, contentBgHeight) + } + + return resultBitmap + } + + /** + * 绘制多行文字 + */ + private fun drawMultilineText( + canvas: Canvas, + text: String, + centerX: Float, + centerY: Float, + paint: Paint, + maxHeight: Int + ) { + val staticLayout = StaticLayout.Builder + .obtain(text, 0, text.length, + paint, (canvas.width * 0.9).toInt()) + .setMaxLines(5) // 限制最大行数 + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .build() + + // 计算垂直居中的起始Y位置 + val totalHeight = staticLayout.height + val startY = if (totalHeight <= maxHeight) { + centerY - totalHeight / 2 + } else { + (canvas.height - maxHeight).toFloat() + } + + canvas.save() + canvas.translate(centerX - staticLayout.width / 2, startY) + staticLayout.draw(canvas) + canvas.restore() + } + + /** + * 生成带时间戳的文件名 + */ + fun generateFileName(subject: String = "巡检报告"): String { + val dateFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault()) + val currentTime = dateFormat.format(Date()) + return "${subject}_${currentTime}.jpg" + } +} \ No newline at end of file