Initial commit: Android Inspection Camera project
This commit is contained in:
339
app/src/main/java/com/inspection/camera/util/ImageProcessor.kt
Normal file
339
app/src/main/java/com/inspection/camera/util/ImageProcessor.kt
Normal file
@@ -0,0 +1,339 @@
|
||||
package com.inspection.camera.util
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import com.inspection.camera.data.models.ImageItem
|
||||
import com.inspection.camera.data.models.ImageQuality
|
||||
import com.inspection.camera.data.models.MergeLayoutType
|
||||
import com.inspection.camera.data.models.WatermarkStyle
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* 图片处理工具类
|
||||
*/
|
||||
object ImageProcessor {
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy年-MM月-dd日 HH:mm:ss", Locale.getDefault())
|
||||
private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault())
|
||||
|
||||
/**
|
||||
* 获取当前时间戳文本
|
||||
*/
|
||||
fun getCurrentTimeText(): String {
|
||||
return dateFormat.format(Date())
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件名
|
||||
*/
|
||||
fun generateFileName(theme: String): String {
|
||||
val timeStr = fileNameFormat.format(Date())
|
||||
return if (theme.isNotBlank()) {
|
||||
"巡检报告_${theme}_$timeStr.jpg"
|
||||
} else {
|
||||
"巡检报告_$timeStr.jpg"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加水印到图片
|
||||
*/
|
||||
fun addWatermark(
|
||||
sourceBitmap: Bitmap,
|
||||
timeText: String,
|
||||
locationText: String,
|
||||
style: WatermarkStyle
|
||||
): Bitmap {
|
||||
val result = sourceBitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||
val canvas = Canvas(result)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
textSize = style.fontSize * result.density
|
||||
color = style.textColor.toArgb()
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
|
||||
val watermarkText = "$timeText $locationText"
|
||||
val textWidth = paint.measureText(watermarkText)
|
||||
val textHeight = paint.fontMetrics.let { it.descent - it.ascent }
|
||||
|
||||
// 计算位置(左下角)
|
||||
val padding = 20f * result.density
|
||||
val x = padding
|
||||
val y = result.height - padding
|
||||
|
||||
// 绘制背景
|
||||
if (style.backgroundColor != android.graphics.Color.TRANSPARENT) {
|
||||
val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = style.backgroundColor.toArgb()
|
||||
}
|
||||
val bgRect = RectF(
|
||||
x - 10,
|
||||
y - textHeight - 10,
|
||||
x + textWidth + 10,
|
||||
y + 10
|
||||
)
|
||||
canvas.drawRoundRect(bgRect, 8f, 8f, bgPaint)
|
||||
}
|
||||
|
||||
// 绘制文字
|
||||
canvas.drawText(watermarkText, x, y, paint)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 合成多张图片
|
||||
*/
|
||||
fun mergeImages(
|
||||
images: List<ImageItem>,
|
||||
layoutType: MergeLayoutType,
|
||||
quality: ImageQuality
|
||||
): Bitmap {
|
||||
if (images.isEmpty()) {
|
||||
return Bitmap.createBitmap(1920, 1080, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
|
||||
val cols = layoutType.cols
|
||||
val rows = layoutType.rows
|
||||
val imageCount = images.size.coerceAtMost(rows * cols)
|
||||
|
||||
val outputWidth = 1920
|
||||
val outputHeight = 1080
|
||||
val cellWidth = outputWidth / cols
|
||||
val cellHeight = outputHeight / rows
|
||||
|
||||
val result = Bitmap.createBitmap(outputWidth, outputHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
canvas.drawColor(Color.WHITE)
|
||||
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
images.forEachIndexed { index, imageItem ->
|
||||
if (index >= rows * cols) return@forEachIndexed
|
||||
|
||||
val col = index % cols
|
||||
val row = index / cols
|
||||
|
||||
val left = col * cellWidth
|
||||
val top = row * cellHeight
|
||||
|
||||
try {
|
||||
val inputStream = imageItem.uri.path?.let { path ->
|
||||
imageItem.uri.let { uri ->
|
||||
inputStream
|
||||
}
|
||||
}
|
||||
|
||||
val sourceBitmap = BitmapFactory.decodeFile(imageItem.path)
|
||||
?: return@forEachIndexed
|
||||
|
||||
// 缩放并居中裁剪
|
||||
val scaledBitmap = scaleAndCropBitmap(sourceBitmap, cellWidth, cellHeight)
|
||||
val dstRect = Rect(left, top, left + cellWidth, top + cellHeight)
|
||||
canvas.drawBitmap(scaledBitmap, null, dstRect, paint)
|
||||
|
||||
if (scaledBitmap != sourceBitmap) {
|
||||
scaledBitmap.recycle()
|
||||
}
|
||||
sourceBitmap.recycle()
|
||||
} catch (e: Exception) {
|
||||
// 加载失败绘制占位
|
||||
val placeholderPaint = Paint().apply {
|
||||
color = Color.LTGRAY
|
||||
}
|
||||
canvas.drawRect(
|
||||
RectF(left.toFloat(), top.toFloat(), (left + cellWidth).toFloat(), (top + cellHeight).toFloat()),
|
||||
placeholderPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放并居中裁剪Bitmap
|
||||
*/
|
||||
private fun scaleAndCropBitmap(source: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
|
||||
val sourceWidth = source.width.toFloat()
|
||||
val sourceHeight = source.height.toFloat()
|
||||
|
||||
val scale = maxOf(
|
||||
targetWidth.toFloat() / sourceWidth,
|
||||
targetHeight.toFloat() / sourceHeight
|
||||
)
|
||||
|
||||
val scaledWidth = sourceWidth * scale
|
||||
val scaledHeight = sourceHeight * scale
|
||||
|
||||
val xOffset = (scaledWidth - targetWidth) / 2
|
||||
val yOffset = (scaledHeight - targetHeight) / 2
|
||||
|
||||
val matrix = Matrix()
|
||||
matrix.postScale(scale, scale)
|
||||
matrix.postTranslate(-xOffset, -yOffset)
|
||||
|
||||
return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加文字到图片
|
||||
*/
|
||||
fun addTextToBitmap(
|
||||
sourceBitmap: Bitmap,
|
||||
title: String,
|
||||
content: String,
|
||||
titleStyle: WatermarkStyle,
|
||||
contentStyle: WatermarkStyle
|
||||
): Bitmap {
|
||||
val result = sourceBitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||
val canvas = Canvas(result)
|
||||
|
||||
val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
textSize = titleStyle.fontSize * result.density
|
||||
color = titleStyle.textColor.toArgb()
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
|
||||
val contentPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
textSize = contentStyle.fontSize * result.density
|
||||
color = contentStyle.textColor.toArgb()
|
||||
typeface = Typeface.DEFAULT
|
||||
}
|
||||
|
||||
val padding = 20f * result.density
|
||||
val titleHeight = titlePaint.fontMetrics.let { it.descent - it.ascent }
|
||||
val lineHeight = contentPaint.fontMetrics.let { it.descent - it.ascent }
|
||||
|
||||
// 顶部标题
|
||||
var y = padding + titleHeight
|
||||
|
||||
if (title.isNotBlank()) {
|
||||
val bgPaint = Paint().apply {
|
||||
color = titleStyle.backgroundColor.toArgb()
|
||||
}
|
||||
val titleWidth = titlePaint.measureText(title)
|
||||
val bgRect = RectF(
|
||||
padding - 10,
|
||||
padding - 10,
|
||||
padding + titleWidth + 10,
|
||||
padding + titleHeight + 10
|
||||
)
|
||||
canvas.drawRoundRect(bgRect, 8f, 8f, bgPaint)
|
||||
canvas.drawText(title, padding, y, titlePaint)
|
||||
y += titleHeight + 20
|
||||
}
|
||||
|
||||
// 底部内容
|
||||
val contentMaxWidth = result.width - padding * 2
|
||||
val contentLines = wrapText(content, contentPaint, contentMaxWidth)
|
||||
var lastY = result.height - padding
|
||||
|
||||
// 先计算内容总高度
|
||||
val contentTotalHeight = contentLines.size * lineHeight + 40 * result.density
|
||||
|
||||
// 从底部向上绘制
|
||||
y = lastY - contentTotalHeight + lineHeight + 20
|
||||
|
||||
if (content.isNotBlank()) {
|
||||
val bgPaint = Paint().apply {
|
||||
color = contentStyle.backgroundColor.toArgb()
|
||||
}
|
||||
val maxLineWidth = contentLines.maxOf { contentPaint.measureText(it) }
|
||||
val bgRect = RectF(
|
||||
padding - 10,
|
||||
y - lineHeight - 10,
|
||||
padding + maxLineWidth + 10,
|
||||
lastY - 20
|
||||
)
|
||||
canvas.drawRoundRect(bgRect, 8f, 8f, bgPaint)
|
||||
|
||||
contentLines.forEach { line ->
|
||||
canvas.drawText(line, padding, y, contentPaint)
|
||||
y += lineHeight
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本自动换行
|
||||
*/
|
||||
private fun wrapText(text: String, paint: Paint, maxWidth: Float): List<String> {
|
||||
val words = text.split("")
|
||||
val lines = mutableListOf<String>()
|
||||
var currentLine = StringBuilder()
|
||||
|
||||
words.forEach { word ->
|
||||
val testLine = if (currentLine.isEmpty()) word else "$currentLine$word"
|
||||
if (paint.measureText(testLine) <= maxWidth) {
|
||||
currentLine = StringBuilder(testLine)
|
||||
} else {
|
||||
if (currentLine.isNotEmpty()) {
|
||||
lines.add(currentLine.toString())
|
||||
}
|
||||
currentLine = StringBuilder(word)
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine.isNotEmpty()) {
|
||||
lines.add(currentLine.toString())
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片到相册
|
||||
*/
|
||||
fun saveToGallery(
|
||||
context: Context,
|
||||
bitmap: Bitmap,
|
||||
fileName: String,
|
||||
quality: Int = 85
|
||||
): Uri? {
|
||||
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.Q) {
|
||||
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/InspectionCamera")
|
||||
put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||
}
|
||||
}
|
||||
|
||||
val resolver = context.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
|
||||
return uri?.let {
|
||||
resolver.openOutputStream(it)?.use { outputStream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
contentValues.clear()
|
||||
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||
resolver.update(it, contentValues, null, null)
|
||||
}
|
||||
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.inspection.camera.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.os.Looper
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationCallback
|
||||
import com.google.android.gms.location.LocationRequest
|
||||
import com.google.android.gms.location.LocationResult
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.location.Priority
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
/**
|
||||
* 位置服务帮助类
|
||||
*/
|
||||
class LocationHelper(private val context: Context) {
|
||||
|
||||
private val fusedLocationClient: FusedLocationProviderClient =
|
||||
LocationServices.getFusedLocationProviderClient(context)
|
||||
|
||||
private val geocoder: Geocoder by lazy {
|
||||
Geocoder(context, Locale.getDefault())
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前位置
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
suspend fun getCurrentLocation(): Location? {
|
||||
return try {
|
||||
fusedLocationClient.lastLocation.await()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据经纬度获取地址
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getAddressFromLocation(latitude: Double, longitude: Double): String {
|
||||
return try {
|
||||
val addresses = geocoder.getFromLocation(latitude, longitude, 1)
|
||||
if (!addresses.isNullOrEmpty()) {
|
||||
val address = addresses[0]
|
||||
buildString {
|
||||
address.locality?.let { append(it) }
|
||||
address.subLocality?.let { if (isNotEmpty()) append(" "); append(it) }
|
||||
address.thoroughfare?.let { if (isNotEmpty()) append(" "); append(it) }
|
||||
}.ifEmpty { "${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}" }
|
||||
} else {
|
||||
"${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取位置信息(地址或经纬度)
|
||||
*/
|
||||
suspend fun getLocationInfo(useNetwork: Boolean = true): String {
|
||||
if (!useNetwork) {
|
||||
val location = getCurrentLocation() ?: return ""
|
||||
return "${"%.4f".format(location.latitude)}, ${"%.4f".format(location.longitude)}"
|
||||
}
|
||||
|
||||
val location = getCurrentLocation() ?: return ""
|
||||
return getAddressFromLocation(location.latitude, location.longitude)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> com.google.android.gms.tasks.Task<T>.await(): T {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
addOnSuccessListener { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
addOnFailureListener { exception ->
|
||||
continuation.resumeWithException(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.inspection.camera.util
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/**
|
||||
* 权限管理工具
|
||||
*/
|
||||
object PermissionManager {
|
||||
|
||||
val cameraPermissions = arrayOf(Manifest.permission.CAMERA)
|
||||
val locationPermissions = arrayOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
|
||||
fun hasCameraPermission(context: Context): Boolean {
|
||||
return cameraPermissions.all {
|
||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
fun hasLocationPermission(context: Context): Boolean {
|
||||
return locationPermissions.any {
|
||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
fun requestCameraPermission(
|
||||
launcher: ActivityResultLauncher<Array<String>>,
|
||||
onResult: (Boolean) -> Unit
|
||||
) {
|
||||
launcher.launch(cameraPermissions)
|
||||
}
|
||||
|
||||
fun requestLocationPermission(
|
||||
launcher: ActivityResultLauncher<Array<String>>,
|
||||
onResult: (Boolean) -> Unit
|
||||
) {
|
||||
launcher.launch(locationPermissions)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user