Add image composition and text overlay functionality
This commit is contained in:
@@ -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 {
|
||||
|
||||
145
app/src/main/java/com/example/app/ImageCompositor.kt
Normal file
145
app/src/main/java/com/example/app/ImageCompositor.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user