From c9f14344621506a2fb7e2a0519730ad49ef60ae4 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Mon, 7 Sep 2026 16:18:28 -0300 Subject: [PATCH] F3.2 Android: agregar pad manuscrito PNG para firmas --- .../korexlabs/dhinspeccion/ui/SignaturePad.kt | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/SignaturePad.kt diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/SignaturePad.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/SignaturePad.kt new file mode 100644 index 0000000..ad1150c --- /dev/null +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/SignaturePad.kt @@ -0,0 +1,138 @@ +package com.korexlabs.dhinspeccion.ui + +import android.graphics.Bitmap +import android.graphics.Canvas as AndroidCanvas +import android.graphics.Color as AndroidColor +import android.graphics.Paint +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.input.pointer.consume +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import java.io.File +import java.io.FileOutputStream + +@Composable +fun SignaturePad( + label: String, + enabled: Boolean = true, + onCaptured: (File) -> Unit, +) { + val context = LocalContext.current + val strokes = remember { mutableStateListOf>() } + var currentStroke by remember { mutableStateOf>(emptyList()) } + var canvasSize by remember { mutableStateOf(IntSize.Zero) } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(label, fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold) + Text( + "Firmá dentro del recuadro. La imagen se guarda como PNG y se incorpora al hash del Acta.", + style = MaterialTheme.typography.bodySmall, + ) + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(190.dp) + .background(Color.White) + .onSizeChanged { canvasSize = it } + .pointerInput(enabled) { + if (!enabled) return@pointerInput + detectDragGestures( + onDragStart = { position -> currentStroke = listOf(position) }, + onDrag = { change, _ -> + change.consume() + currentStroke = currentStroke + change.position + }, + onDragEnd = { + if (currentStroke.size > 1) strokes.add(currentStroke) + currentStroke = emptyList() + }, + onDragCancel = { currentStroke = emptyList() }, + ) + }, + ) { + val all = strokes + listOf(currentStroke) + all.forEach { stroke -> + stroke.zipWithNext().forEach { (start, end) -> + drawLine( + color = Color.Black, + start = start, + end = end, + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round, + ) + } + } + } + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton( + onClick = { strokes.clear(); currentStroke = emptyList() }, + enabled = enabled && (strokes.isNotEmpty() || currentStroke.isNotEmpty()), + modifier = Modifier.weight(1f), + ) { Text("Limpiar") } + Button( + onClick = { + val width = canvasSize.width.coerceAtLeast(1) + val height = canvasSize.height.coerceAtLeast(1) + val targetWidth = 1000 + val targetHeight = 400 + val scaleX = targetWidth.toFloat() / width.toFloat() + val scaleY = targetHeight.toFloat() / height.toFloat() + val bitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888) + val native = AndroidCanvas(bitmap) + native.drawColor(AndroidColor.WHITE) + val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = AndroidColor.BLACK + strokeWidth = 7f + strokeCap = Paint.Cap.ROUND + strokeJoin = Paint.Join.ROUND + style = Paint.Style.STROKE + } + strokes.forEach { stroke -> + stroke.zipWithNext().forEach { (start, end) -> + native.drawLine( + start.x * scaleX, + start.y * scaleY, + end.x * scaleX, + end.y * scaleY, + paint, + ) + } + } + val file = File.createTempFile("DH_FIRMA_", ".png", context.cacheDir) + FileOutputStream(file).use { stream -> + check(bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) + } + bitmap.recycle() + onCaptured(file) + }, + enabled = enabled && strokes.isNotEmpty(), + modifier = Modifier.weight(1f), + ) { Text("Usar firma") } + } + } +}