Add image composition and text overlay functionality

This commit is contained in:
xiajiid
2026-02-08 12:16:31 +08:00
parent ce8c4f51fd
commit ad950e8468
2 changed files with 298 additions and 6 deletions

View File

@@ -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<Uri>()
// 图片合成器
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<Bitmap>()
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 {

View File

@@ -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<Bitmap>, 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"
}
}