使用compose实现签名、签名缩放、签名转为图片实现
·
文章目录
1、实现效果:
1.1 签名

1.2 将签名转为图片展示

2、关键实现步骤
2.1 绘制“米”字辅助线
/**
* 绘制米字辅助线
*/
@Composable
fun BoxScope.DrawGrid() {
Canvas(
modifier = Modifier
.matchParentSize()
.background(color = colorResource(R.color.page_color))
) {
val width = size.width
val height = size.height
val canvasStrokeWidth = 1f
val gridColor = Color(0xFF062C6B)
// 水平线
for (i in 1 until 2) {
val y = height * i / 2
drawLine(
color = gridColor,
start = Offset(0f, y),
end = Offset(width, y),
strokeWidth = canvasStrokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
)
}
// 垂直线
for (i in 1 until 2) {
val x = width * i / 2
drawLine(
color = gridColor,
start = Offset(x, 0f),
end = Offset(x, height),
strokeWidth = canvasStrokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
)
}
// 对角线
drawLine(
color = gridColor,
start = Offset(10f, 10f),
end = Offset(width - 10f, height - 10f),
strokeWidth = canvasStrokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
)
drawLine(
color = gridColor,
start = Offset(width - 10f, 10f),
end = Offset(10f, height - 10f),
strokeWidth = canvasStrokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
)
}
}
对于对角线的绘制,设置一定向内偏移量,为了避免线条与圆角重叠的问题,使用Canvas中drawLine进行线头的绘制
2.1.1 drawLine的解析
drawLine(
color = gridColor, // 线条颜色
start = Offset(10f, 10f), // 起点坐标
end = Offset(width - 10f, height - 10f), // 终点坐标
strokeWidth = canvasStrokeWidth, // 线条宽度
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
// 路径效果,用于改变线条的样式。使用了PathEffect.dashPathEffect来创建虚线效果
)
除了虚线效果,PathEffect 还支持其他效果:
圆角效果
pathEffect = PathEffect.cornerPathEffect(10f) // 10px圆角
离线效果(抖动)
pathEffect = PathEffect.discretePathEffect(5f, 10f) // 5px段长,10px随机偏移
2.2 绘制签名
在一定范围内绘制签名,超出规定范围的签名不进行绘制
// 绘制签名
Canvas(
modifier = Modifier
.matchParentSize()
.padding(dimensionResource(R.dimen.padding_10))
.onSizeChanged { size -> contentSize = size }
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
// 确保起点在边界内
val clampedOffset = Offset(
offset.x.coerceIn(0f, contentSize.width.toFloat()),
offset.y.coerceIn(0f, contentSize.height.toFloat())
)
state.startNewStroke(clampedOffset)
},
onDrag = { change, _ ->
val position = change.position
// 自动截断到边界内
val clampedPosition = Offset(
position.x.coerceIn(0f, contentSize.width.toFloat()),
position.y.coerceIn(0f, contentSize.height.toFloat())
)
// 添加截断后的点
state.addPointToCurrentStroke(clampedPosition)
},
onDragEnd = {
state.completeStroke()
}
)
}
) {
// 绘制背景
drawRect(Color.Transparent)
// 绘制当前字符
state.currentCharacter.paths.forEach { path ->
drawPath(
path = path,
color = Color.Blue,
style = Stroke(width = 4f)
)
}
// 绘制当前笔画
if (state.currentStroke.isNotEmpty()) {
val path = Path().apply {
moveTo(state.currentStroke.first().x, state.currentStroke.first().y)
state.currentStroke.drop(1).forEach { point ->
lineTo(point.x, point.y)
}
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(width = 4f)
)
}
}
通过Modifier.pointerInput和Modifier.onSizeChanged完成变成的限制
2.2.1 签名限制边界的实现
.onSizeChanged { size -> contentSize = size }
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
// 确保起点在边界内
val clampedOffset = Offset(
offset.x.coerceIn(0f, contentSize.width.toFloat()),
offset.y.coerceIn(0f, contentSize.height.toFloat())
)
state.startNewStroke(clampedOffset)
},
onDrag = { change, _ ->
val position = change.position
// 自动截断到边界内
val clampedPosition = Offset(
position.x.coerceIn(0f, contentSize.width.toFloat()),
position.y.coerceIn(0f, contentSize.height.toFloat())
)
// 添加截断后的点
state.addPointToCurrentStroke(clampedPosition)
},
onDragEnd = {
state.completeStroke()
}
)
}
2.3 缩放绘制签名
在绘制签名的路径之前,添加withTransform方法进行缩放操作,可自定义
character.paths.forEach { path ->
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
scale(1 / 7f, 1 / 7f, pivot = Offset.Zero)
}) {
drawPath(path, Color.Black, style = Stroke(2f))
}
}
}
2.4 将签名绘制路径转为图片
在绘制之前确定每个字符的位置和大小,将其绘制在一个ImageBitmap中
2.4.1 确认字符的位置和大小
用于捕获每个字符的位置和大小
val characterSizes = remember { mutableMapOf<Int, IntSize>() }
val characterPositions = remember { mutableMapOf<Int, Offset>() }
val coroutine = rememberCoroutineScope()
在最外层中获取字符绘制路径并进行图片转换
对于路径的转换需要文件的写入,创建位图需要在后台操作,所以在协程中进行
Modifier.onSizeChanged { size ->
// 生成图片
coroutine.launch {
generateImage(
context = context,
characters = characters,
characterSizes = characterSizes,
characterPositions = characterPositions,
containerSize = size
) { image ->
generatedImage = image
imageCallBack.invoke(generatedImage)
}
}
},
在每个字符绘制中进行定位
MOdifier.onSizeChanged { size ->
characterSizes[index] = size
}
.onPlaced { layoutCoordinates ->
// 获取字符在容器中的位置
val position = layoutCoordinates.positionInParent()
characterPositions[index] = position
}
2.4.2 generateImage的实现
/**
* 签名生成图片方法
*/
private suspend fun generateImage(
context: Context,
characters: List<SignatureCharacter>,
characterSizes: Map<Int, IntSize>,
characterPositions: Map<Int, Offset>,
containerSize: IntSize,
onImageGenerated: (ImageBitmap) -> Unit
) {
// 确保所有字符的位置和大小都已捕获
if (characterSizes.size != characters.size || characterPositions.size != characters.size) {
return
}
// 在后台线程执行生成操作
withContext(Dispatchers.IO) {
// 创建位图
val bitmap = Bitmap.createBitmap(
containerSize.width,
containerSize.height,
Bitmap.Config.ARGB_8888
)
val androidCanvas = android.graphics.Canvas(bitmap)
// 绘制背景
androidCanvas.drawColor(android.graphics.Color.WHITE)
// 创建画笔
val paint = android.graphics.Paint().apply {
color = android.graphics.Color.BLACK
style = android.graphics.Paint.Style.STROKE
strokeWidth = 2f
isAntiAlias = true
}
// 绘制所有字符
characters.forEachIndexed { index, character ->
val position = characterPositions[index] ?: return@forEachIndexed
val size = characterSizes[index] ?: return@forEachIndexed
// 绘制每个路径
character.paths.forEach { path ->
// 应用缩放变换
val matrix = android.graphics.Matrix()
matrix.postScale(1 / 7f, 1 / 7f, 0f, 0f)
// 创建变换后的路径
val transformedPath = android.graphics.Path(path.asAndroidPath())
transformedPath.transform(matrix)
// 平移到正确位置
val translateMatrix = android.graphics.Matrix()
translateMatrix.postTranslate(position.x, position.y)
transformedPath.transform(translateMatrix)
// 绘制路径
androidCanvas.drawPath(transformedPath, paint)
}
}
// 转换为Compose ImageBitmap
val imageBitmap = bitmap.asImageBitmap()
onImageGenerated(imageBitmap)
// 保存到文件
saveBitmapToFile(context, bitmap)
}
}
3、完整代码实现
package cn.lisi.chainofcustody.ui.component
import android.content.Context
import android.graphics.Bitmap
import android.media.MediaScannerConnection
import android.os.Build
import android.os.Environment
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.annotation.RequiresApi
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cn.lisi.chainofcustody.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 签名页面:预览、签名区域、按钮
* @param imageCallBack 图片,图片路径,是否为默认的签名
*/
@Composable
fun SignatureScreenPage(
index:Int = 0,
imageCallBack: (ImageBitmap?, String?,Boolean) -> Unit = {_,_,_-> },
back:()->Unit = {}) {
// 签名状态
var imageLocalBitmap by remember { mutableStateOf<Bitmap?>(null) }
val signatureState = rememberSignatureState()
BackHandler {
back.invoke()
}
Column(
modifier = Modifier
.background(colorResource(R.color.page_color))
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_10))
) {
// 预览区域 state.completedCharacters
SignaturePreview(
index = index,
characters = signatureState.completedCharacters,
modifier = Modifier
.fillMaxWidth()
.height(dimensionResource(R.dimen.height_80)),
imageCallBack = {image->
imageLocalBitmap = image
val imageBitmap = image.asImageBitmap()
signatureState.generatedImage = imageBitmap
}
)
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_15)))
// 书写区域
SignatureCanvas(
state = signatureState,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_20)))
// 提示文字
Text(
text = "请在书写区逐一书写全名",
color = Color(0xFF666666),
fontSize = dimensionResource(R.dimen.text_size_15).value.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_20)))
// 签署规则说明
SignatureRules()
// 操作按钮
SignatureControls(
bitmap = imageLocalBitmap,
index = index,
state = signatureState,
modifier = Modifier
.fillMaxWidth()
.padding(top = dimensionResource(R.dimen.padding_42)),
imageCallBack = imageCallBack
)
}
}
/**
* 签名规则说明
*/
@Composable
fun SignatureRules() {
Column {
Text(
text = "签署框为单字签署,每个字签署结束点击【完成当前】将出现在屏幕上方,方可继续签署下一个字。全名签署完成后,再点击【保存】。点击【撤销】可重写单字",
color = Color(0xFF666666),
fontSize = dimensionResource(R.dimen.text_size_12).value.sp,
)
}
}
/**
* 签名预览页面
*/
@Composable
fun SignaturePreview(
index: Int = 0,
characters: List<SignatureCharacter>,
modifier: Modifier = Modifier,
imageCallBack: (Bitmap) -> Unit,
) {
val context = LocalContext.current
// var generatedImage by remember { mutableStateOf<ImageBitmap?>(null) }
// 用于捕获每个字符的位置和大小
val characterSizes = remember(characters) { mutableMapOf<Int,IntSize>() }
val characterPositions = remember(characters) { mutableMapOf<Int, Offset>() }
val coroutine = rememberCoroutineScope()
Box(
modifier = modifier
.background(Color.White, RoundedCornerShape(dimensionResource(R.dimen.round_corner)))
.fillMaxWidth()
.height(dimensionResource(R.dimen.height_80))
.padding(dimensionResource(R.dimen.padding_5))
// .border(
// dimensionResource(R.dimen.stroke_width),
// Color.Gray,
// RoundedCornerShape(dimensionResource(R.dimen.round_corner))
// )
.onSizeChanged { size ->
Log.d(TAG, "SignaturePreview: size $size")
// 生成图片
coroutine.launch {
generateImage(
// index = index,
// context = context,
characters = characters,
characterSizes = characterSizes,
characterPositions = characterPositions,
containerSize = size
) { image ->
Log.d(TAG, "SignaturePreview:image $image")
// generatedImage = image
imageCallBack.invoke(image)
}
}
},
contentAlignment = Alignment.CenterStart
) {
if (characters.isEmpty()) {
Text(
"签名预览区域",
color = Color.Gray,
modifier = Modifier.align(Alignment.Center)
)
} else
Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier.matchParentSize()
) {
characters.forEachIndexed { index, character ->
Box(
modifier = Modifier
.padding(horizontal = dimensionResource(R.dimen.padding_5))
.width(dimensionResource(R.dimen.padding_30))
.height(dimensionResource(R.dimen.height_80))
.onSizeChanged { size ->
Log.d(TAG, "SignaturePreview: $size")
characterSizes[index] = size
}
.onPlaced { layoutCoordinates ->
// 获取字符在容器中的位置
val position = layoutCoordinates.positionInParent()
characterPositions[index] = position
}
) {
character.paths.forEach { path ->
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
scale(1 / 7f, 1 / 7f, pivot = Offset.Zero)
}) {
drawPath(path, Color.Black, style = Stroke(2f))
}
}
}
}
}
}
}
}
/**
* 签名生成图片方法
*/
private suspend fun generateImage(
characters: List<SignatureCharacter>,
characterSizes: Map<Int, IntSize>,
characterPositions: Map<Int, Offset>,
containerSize: IntSize,
onImageGenerated: (Bitmap) -> Unit
) {
// 确保所有字符的位置和大小都已捕获
Log.d(TAG, "generateImage: ${characterSizes.size} ${characterPositions.size} ${characters.size}")
if (characterSizes.size != characters.size || characterPositions.size != characters.size) {
return
}
// 在后台线程执行生成操作
withContext(Dispatchers.IO) {
// 创建位图
val bitmap = Bitmap.createBitmap(
containerSize.width,
containerSize.height,
Bitmap.Config.ARGB_8888
)
Log.d(TAG, "generateImage: width ${containerSize.width} \n height ${containerSize.height} $bitmap")
val androidCanvas = android.graphics.Canvas(bitmap)
// 绘制背景
androidCanvas.drawColor(android.graphics.Color.WHITE)
// 创建画笔
val paint = android.graphics.Paint().apply {
color = android.graphics.Color.BLACK
style = android.graphics.Paint.Style.STROKE
strokeWidth = 2f
isAntiAlias = true
}
// 绘制所有字符
characters.forEachIndexed { index, character ->
val position = characterPositions[index] ?: return@forEachIndexed
val size = characterSizes[index] ?: return@forEachIndexed
Log.d(TAG, "generateImage: position $position")
// 绘制每个路径
character.paths.forEach { path ->
// 应用缩放变换
val matrix = android.graphics.Matrix()
matrix.postScale(1 / 7f, 1 / 7f, 0f, 0f)
// 创建变换后的路径
val transformedPath = android.graphics.Path(path.asAndroidPath())
transformedPath.transform(matrix)
// 平移到正确位置
val translateMatrix = android.graphics.Matrix()
translateMatrix.postTranslate(position.x, position.y)
transformedPath.transform(translateMatrix)
// 绘制路径
androidCanvas.drawPath(transformedPath, paint)
}
}
onImageGenerated(bitmap)
}
}
private suspend fun generateImage1(
context: Context,
characters: List<SignatureCharacter>,
characterSizes: Map<Int, IntSize>,
characterPositions: Map<Int, Offset>,
containerSize: IntSize,
onImageGenerated: (Bitmap) -> Unit
) {
if (characterSizes.size != characters.size || characterPositions.size != characters.size) {
return
}
withContext(Dispatchers.IO) {
try {
// 1. 创建位图 - 使用预览区域的实际尺寸
val density = context.resources.displayMetrics.density
val previewWidth = containerSize.width
val previewHeight = containerSize.height
val bitmap = Bitmap.createBitmap(
previewWidth,
previewHeight,
Bitmap.Config.ARGB_8888
)
val canvas = android.graphics.Canvas(bitmap)
// 2. 绘制白色背景
canvas.drawColor(android.graphics.Color.WHITE)
// 3. 创建画笔 - 增加线宽确保可见
val paint = android.graphics.Paint().apply {
color = android.graphics.Color.BLACK
style = android.graphics.Paint.Style.STROKE
strokeWidth = 4f * density // 使用密度缩放
isAntiAlias = true
strokeCap = android.graphics.Paint.Cap.ROUND
}
// 4. 绘制所有字符
characters.forEachIndexed { charIndex, character ->
val position = characterPositions[charIndex] ?: return@forEachIndexed
val size = characterSizes[charIndex] ?: return@forEachIndexed
// 5. 直接使用原始路径,不进行额外缩放
character.paths.forEach { composePath ->
// 转换为 Android Path
val androidPath = composePath.asAndroidPath()
// 6. 应用位置偏移
val matrix = android.graphics.Matrix()
matrix.postScale(1 / 7f, 1 / 7f, 0f, 0f)
// val matrix = android.graphics.Matrix()
matrix.postTranslate(position.x, position.y)
androidPath.transform(matrix)
// 7. 绘制路径
canvas.drawPath(androidPath, paint)
}
}
onImageGenerated(bitmap)
} catch (e: Exception) {
Log.e(TAG, "generateImage error: ${e.message}", e)
}
}
}
/**
* 保存图片到本地
*/
private fun saveBitmapToFile(
context: Context,
index: Int,
bitmap: Bitmap,
savePath: (String) -> Unit
) {
try {
// 确保位图是可变的
val mutableBitmap = if (bitmap.isMutable) {
bitmap
} else {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
}
// 生成唯一文件名
val filename = "signature_user_entrust_${index}.png"
// 获取存储目录
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
storageDir?.mkdirs()
val file = File(storageDir, filename)
// 使用缓冲流保存
BufferedOutputStream(FileOutputStream(file), 8192).use { stream ->
if (!mutableBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
Log.e(TAG, "Failed to compress bitmap")
return
}
stream.flush()
}
// 通知媒体库
MediaScannerConnection.scanFile(
context,
arrayOf(file.absolutePath),
arrayOf("image/png"),
null
)
// 返回文件路径
savePath(file.absolutePath)
Log.i(TAG, "Bitmap saved successfully: ${file.absolutePath}")
} catch (e: Exception) {
Log.e(TAG, "saveBitmapToFile error: ${e.message}", e)
}
}
/**
* 绘制米字辅助线
*/
@Composable
fun BoxScope.DrawGrid() {
Canvas(
modifier = Modifier
.matchParentSize()
.background(color = colorResource(R.color.page_color))
) {
val width = size.width
val height = size.height
val canvasStrokeWidth = 1f
val gridColor = Color(0xFF062C6B)
// 水平线
for (i in 1 until 2) {
val y = height * i / 2
drawLine(
color = gridColor,
start = Offset(0f, y),
end = Offset(width, y),
strokeWidth = canvasStrokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
)
}
// 垂直线
for (i in 1 until 2) {
val x = width * i / 2
drawLine(
color = gridColor,
start = Offset(x, 0f),
end = Offset(x, height),
strokeWidth = canvasStrokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
)
}
// 对角线
drawLine(
color = gridColor,
start = Offset(10f, 10f),
end = Offset(width - 10f, height - 10f),
strokeWidth = canvasStrokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
)
drawLine(
color = gridColor,
start = Offset(width - 10f, 10f),
end = Offset(10f, height - 10f),
strokeWidth = canvasStrokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
)
}
}
/**
* 网格线+签名
*/
@Composable
fun SignatureCanvas(
state: SignatureState,
modifier: Modifier = Modifier
) {
var contentSize by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier = modifier
.background(Color.White, RoundedCornerShape(dimensionResource(R.dimen.round_corner)))
.border(
dimensionResource(R.dimen.stroke_width),
color = colorResource(R.color.remark_text_color),
RoundedCornerShape(dimensionResource(R.dimen.round_corner))
)
) {
// 绘制网格线
// 绘制米字辅助线
DrawGrid()
// 绘制签名
Canvas(
modifier = Modifier
.matchParentSize()
.padding(dimensionResource(R.dimen.padding_10))
.onSizeChanged { size -> contentSize = size }
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
// 确保起点在边界内
val clampedOffset = Offset(
offset.x.coerceIn(0f, contentSize.width.toFloat()),
offset.y.coerceIn(0f, contentSize.height.toFloat())
)
state.startNewStroke(clampedOffset)
},
onDrag = { change, _ ->
val position = change.position
// 自动截断到边界内
val clampedPosition = Offset(
position.x.coerceIn(0f, contentSize.width.toFloat()),
position.y.coerceIn(0f, contentSize.height.toFloat())
)
// 添加截断后的点
state.addPointToCurrentStroke(clampedPosition)
},
onDragEnd = {
state.completeStroke()
}
)
}
) {
// 绘制背景
drawRect(Color.Transparent)
// 绘制当前字符
state.currentCharacter.paths.forEach { path ->
drawPath(
path = path,
color = Color.Blue,
style = Stroke(width = 4f)
)
}
// 绘制当前笔画
if (state.currentStroke.isNotEmpty()) {
val path = Path().apply {
moveTo(state.currentStroke.first().x, state.currentStroke.first().y)
state.currentStroke.drop(1).forEach { point ->
lineTo(point.x, point.y)
}
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(width = 4f)
)
}
}
// 当前字符提示
if (state.completedCharacters.isNotEmpty() || state.currentCharacter.paths.isNotEmpty()) {
Text(
text = "正在书写第 ${state.completedCharacters.size + 1} 个字",
color = Color.Gray,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(dimensionResource(R.dimen.padding_10))
)
}
}
}
/**
* 签名按钮控制
*/
@Composable
fun SignatureControls(
bitmap: Bitmap?,
index: Int = 0,
state: SignatureState,
modifier: Modifier = Modifier,
imageCallBack: (ImageBitmap?, String?,Boolean) -> Unit
) {
val coroutine = rememberCoroutineScope()
val context = LocalContext.current
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween
) {
// 清除当前字
CustomRedButton(
modifier = Modifier.weight(1f),
buttonText = "撤销",
buttonClick = {
if (state.currentCharacter.paths.isNotEmpty()) {
state.clearCurrentCharacter()
} else {
if (state.canUndo) {
if (Build.VERSION.SDK_INT >= 35) {
state.undo()
}
}
}
},
enable = state.currentCharacter.paths.isNotEmpty(),
)
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_15)))
CustomRedButton(
modifier = Modifier.weight(1f),
buttonText = "完成当前",
buttonClick = { state.completeCurrentCharacter() },
enable = state.currentCharacter.paths.isNotEmpty(),
)
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_15)))
var isSave by remember { mutableStateOf(false) }
CustomRedButton(
modifier = Modifier.weight(1f),
buttonText = "保存",
buttonClick = {
coroutine.launch {
// 保存到文件
if (bitmap != null) {
saveBitmapToFile(
context = context,
bitmap = bitmap,
index = index,
savePath = {path->
state.imagePath = path
isSave = true
})
}else{
isSave = true
}
}
},
enable = state.completedCharacters.isNotEmpty() && state.currentCharacter.paths.isEmpty() && state.generatedImage != null,
)
if (isSave && state.generatedImage != null) {
CustomContentDialog {
Column(
modifier = Modifier
.background(
color = colorResource(R.color.page_color),
shape = CircleShape.copy(
CornerSize(dimensionResource(R.dimen.round_corner))
)
)
.padding(dimensionResource(R.dimen.padding_20))
) {
Text(
text = "手写签名外观确认",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Box(modifier = Modifier
.padding(vertical = dimensionResource(R.dimen.padding_20))
.border(
width = dimensionResource(R.dimen.stroke_width),
color = colorResource(
R.color.remark_text_color
)
)
.fillMaxWidth()
.height(
dimensionResource(R.dimen.height_80)
)
){
if (state.generatedImage != null)
Image(
state.generatedImage!!,
contentDescription = "",
modifier = Modifier
.matchParentSize()
)
}
var isCheckout by remember { mutableStateOf(false) }
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
CustomCheckout(isCheckout = isCheckout) {
isCheckout = !isCheckout
}
Text(
text = "授权快速签署:",
fontWeight = FontWeight.Bold,
fontSize = dimensionResource(R.dimen.text_size_12).value.sp
)
Text(
text = "保存签名,后续使用签名快速进行委托签署",
fontSize = dimensionResource(R.dimen.text_size_8).value.sp
)
}
Row(modifier = Modifier.padding(dimensionResource(R.dimen.padding_20))) {
CustomRedButton(
enable = true,
buttonColor = colorResource(R.color.button_text_disable_color),
modifier = Modifier
.weight(1f)
.height(dimensionResource(R.dimen.height_40))
.padding(end = dimensionResource(R.dimen.padding_15)),
buttonText = "重新设置",
buttonClick = {
state.cleanAllPathPoint()
isSave = false
// state.generatedImage = null
}
)
CustomRedButton(
modifier = Modifier
.weight(1f)
.height(dimensionResource(R.dimen.height_40))
.padding(start = dimensionResource(R.dimen.padding_15)),
buttonText = "提交",
enable = true,
buttonClick = {
imageCallBack.invoke(state.generatedImage,state.imagePath,isCheckout)
state.cleanAllPathPoint()
isSave = false
// state.generatedImage = null
//todo 保存,可先保存在本地,结束该页面
}
)
}
}
}
}else if (isSave && state.generatedImage == null){
val context = LocalContext.current
Toast.makeText(context, "请重试", Toast.LENGTH_SHORT).show()
}
}
}
class SignatureState {
// 已完成的字符列表
var completedCharacters by mutableStateOf(emptyList<SignatureCharacter>())
// 当前正在书写的字符
var currentCharacter by mutableStateOf(SignatureCharacter())
// 当前笔画
var currentStroke by mutableStateOf(emptyList<Offset>())
// 操作历史(用于撤销)
private var history = mutableListOf<SignatureCharacter>()
var generatedImage by mutableStateOf<ImageBitmap?>(null)
var imagePath by mutableStateOf<String?>(null)
// 是否可以撤销
val canUndo get() = history.isNotEmpty()
// 开始新笔画
fun startNewStroke(startPoint: Offset) {
currentStroke = listOf(startPoint)
}
// 添加点到当前笔画
fun addPointToCurrentStroke(point: Offset) {
currentStroke = currentStroke + point
}
/**
* 清空
*/
fun cleanAllPathPoint() {
currentStroke = emptyList()
currentCharacter = SignatureCharacter()
completedCharacters = emptyList()
history = emptyList<SignatureCharacter>().toMutableList()
generatedImage = null
}
// 完成当前笔画
fun completeStroke() {
if (currentStroke.size > 1) {
val path = Path().apply {
moveTo(currentStroke.first().x, currentStroke.first().y)
currentStroke.drop(1).forEach { point ->
lineTo(point.x, point.y)
}
}
currentCharacter = currentCharacter.copy(paths = currentCharacter.paths + path)
}
currentStroke = emptyList()
}
// 完成当前字符
fun completeCurrentCharacter() {
if (currentCharacter.paths.isNotEmpty()) {
// 保存历史用于撤销
history.add(currentCharacter)
completedCharacters = completedCharacters + currentCharacter
currentCharacter = SignatureCharacter()
}
}
// 清除当前字符
fun clearCurrentCharacter() {
if (currentCharacter.paths.isNotEmpty()) {
// 保存历史用于撤销
history.add(currentCharacter)
currentCharacter = SignatureCharacter()
currentStroke = emptyList()
}
}
// 撤销操作
@RequiresApi(35)
fun undo() {
if (history.isNotEmpty()) {
val lastAction = history.removeLast()
if (completedCharacters.isNotEmpty() && completedCharacters.last() == lastAction) {
// 撤销完成字符
completedCharacters = completedCharacters.dropLast(1)
} else {
// 撤销清除操作
currentCharacter = lastAction
}
}
}
}
data class SignatureCharacter(
val paths: List<Path> = emptyList()
)
@Composable
fun rememberSignatureState(): SignatureState {
return remember { SignatureState() }
}
// 预览
@Preview(showBackground = true)
@Composable
fun SignaturePagePreview() {
MaterialTheme {
SignatureScreenPage()
}
}
补充bug修改
在测试过程中,发现出现签名无法转为图片的问题,偶尔出现
解决:
经过多轮测试加打印日志发现,是位置列表和大小列表重新签名的时候没有重置,出现大小不匹配的问题,在generateImage中出现问题,直接return,解决代码见下:
// 用于捕获每个字符的位置和大小
val characterSizes = remember(characters) { mutableMapOf<Int,IntSize>() }
val characterPositions = remember(characters) { mutableMapOf<Int, Offset>() }
更多推荐


所有评论(0)