android 手工签名,(电子签名)
先上效果图:
实现
自定义一个view
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.RectF
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewTreeObserver
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.math.absoluteValue
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/*
* @Description: SignaturePad 手工签名
* @Version: v1.0
* @Author: Lani.wong
* @Date: 2024-12-09 16:20
*/
class SignaturePad(context: Context, attrs: AttributeSet?) :
View(context, attrs) {
//View state
private var mPoints: MutableList<TimedPoint>? = null
private var mIsEmpty = false
private var mHasEditState: Boolean? = null
private var mLastTouchX = 0f
private var mLastTouchY = 0f
private var mLastVelocity = 0f
private var mLastWidth = 0f
private val mDirtyRect: RectF
private var mBitmapSavedState: Bitmap? = null
private val mSvgBuilder = SvgBuilder()
// Cache
private val mPointsCache: MutableList<TimedPoint> = ArrayList()
private val mControlTimedPointsCached = ControlTimedPoints()
private val mBezierCached = Bezier()
//Configurable parameters
private var mMinWidth = 0
private var mMaxWidth = 0
private var mVelocityFilterWeight = 0f
private var mOnSignedListener: OnSignedListener? = null
private var mClearOnDoubleClick = false
//Double click detector
private val mGestureDetector: GestureDetector
//Default attribute values
private val DEFAULT_ATTR_PEN_MIN_WIDTH_PX = 3
private val DEFAULT_ATTR_PEN_MAX_WIDTH_PX = 7
private val DEFAULT_ATTR_PEN_COLOR = Color.BLACK
private val DEFAULT_ATTR_VELOCITY_FILTER_WEIGHT = 0.9f
private val DEFAULT_ATTR_CLEAR_ON_DOUBLE_CLICK = false
private val mPaint = Paint()
private var mSignatureBitmap: Bitmap? = null
private var mSignatureBitmapCanvas: Canvas? = null
//路径
// private val paths: List<Path>? = null
//文件夹
private val dir = "Signature"
init {
val a = context.theme.obtainStyledAttributes(
attrs,
R.styleable.SignaturePad,
0, 0
)
//Configurable parameters
try {
mMinWidth = a.getDimensionPixelSize(
R.styleable.SignaturePad_penMinWidth,
convertDpToPx(DEFAULT_ATTR_PEN_MIN_WIDTH_PX.toFloat())
)
mMaxWidth = a.getDimensionPixelSize(
R.styleable.SignaturePad_penMaxWidth,
convertDpToPx(DEFAULT_ATTR_PEN_MAX_WIDTH_PX.toFloat())
)
mPaint.color = a.getColor(R.styleable.SignaturePad_penColor, DEFAULT_ATTR_PEN_COLOR)
mVelocityFilterWeight = a.getFloat(
R.styleable.SignaturePad_velocityFilterWeight,
DEFAULT_ATTR_VELOCITY_FILTER_WEIGHT
)
mClearOnDoubleClick = a.getBoolean(
R.styleable.SignaturePad_clearOnDoubleClick,
DEFAULT_ATTR_CLEAR_ON_DOUBLE_CLICK
)
} finally {
a.recycle()
}
//Fixed parameters
mPaint.isAntiAlias = true
mPaint.style = Paint.Style.STROKE
mPaint.strokeCap = Paint.Cap.ROUND
mPaint.strokeJoin = Paint.Join.ROUND
//Dirty rectangle to update only the changed portion of the view
mDirtyRect = RectF()
clearView()
mGestureDetector =
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
return onDoubleClick()
}
})
}
override fun onSaveInstanceState(): Parcelable {
try {
val bundle = Bundle()
bundle.putParcelable("superState", super.onSaveInstanceState())
if (this.mHasEditState == null || mHasEditState!!) {
this.mBitmapSavedState = this.transparentSignatureBitmap
}
bundle.putParcelable("signatureBitmap", this.mBitmapSavedState)
return bundle
} catch (e: Exception) {
Log.w(TAG, String.format("error saving instance state: %s", e.message))
return super.onSaveInstanceState()!!
}
}
override fun onRestoreInstanceState(state: Parcelable) {
var state: Parcelable? = state
if (state is Bundle) {
val bundle = state
// this.setSignatureBitmap((Bitmap) bundle.getParcelable("signatureBitmap"));
// this.mBitmapSavedState = bundle.getParcelable("signatureBitmap");
(bundle.getParcelable<Parcelable>("signatureBitmap") as Bitmap?)?.let {
setTheSignature(
it
)
}
this.mBitmapSavedState = bundle.getParcelable("signatureBitmap")
state = bundle.getParcelable("superState")
}
this.mHasEditState = false
super.onRestoreInstanceState(state)
}
/**
* Set the pen color from a given resource.
* If the resource is not found, [android.graphics.Color.BLACK] is assumed.
*
* @param colorRes the color resource.
*/
fun setPenColorRes(colorRes: Int) {
try {
setPenColor(resources.getColor(colorRes))
} catch (ex: Resources.NotFoundException) {
setPenColor(Color.parseColor("#000000"))
}
}
/**
* Set the pen color from a given color.
*
* @param color the color.
*/
fun setPenColor(color: Int) {
mPaint.color = color
}
/**
* Set the minimum width of the stroke in pixel.
*
* @param minWidth the width in dp.
*/
fun setMinWidth(minWidth: Float) {
mMinWidth = convertDpToPx(minWidth)
mLastWidth = (mMinWidth + mMaxWidth) / 2f
}
/**
* Set the maximum width of the stroke in pixel.
*
* @param maxWidth the width in dp.
*/
fun setMaxWidth(maxWidth: Float) {
mMaxWidth = convertDpToPx(maxWidth)
mLastWidth = (mMinWidth + mMaxWidth) / 2f
}
/**
* Set the velocity filter weight.
*
* @param velocityFilterWeight the weight.
*/
fun setVelocityFilterWeight(velocityFilterWeight: Float) {
mVelocityFilterWeight = velocityFilterWeight
}
var nodeCountX: Float = 0f
var nodeCountY: Float = 0f
var downX: Float = 0f
var downY: Float = 0f
fun clearView() {
nodeCountX = 0f
nodeCountY = 0f
downY = 0f
downY = 0f
mSvgBuilder.clear()
mPoints = ArrayList()
mLastVelocity = 0f
mLastWidth = (mMinWidth + mMaxWidth) / 2f
if (mSignatureBitmap != null) {
mSignatureBitmap = null
ensureSignatureBitmap()
}
isEmpty = true
invalidate()
}
fun clear() {
this.clearView()
this.mHasEditState = true
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isEnabled) return false
val eventX = event.x
val eventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
mPoints!!.clear()
if (mGestureDetector.onTouchEvent(event)) {
// break
} else {
mLastTouchX = eventX
mLastTouchY = eventY
downX = eventX
downY = eventY
addPoint(getNewPoint(eventX, eventY))
if (mOnSignedListener != null) mOnSignedListener!!.onStartSigning()
resetDirtyRect(eventX, eventY)
addPoint(getNewPoint(eventX, eventY))
isEmpty = false
}
}
MotionEvent.ACTION_MOVE -> {
resetDirtyRect(eventX, eventY)
addPoint(getNewPoint(eventX, eventY))
isEmpty = false
}
MotionEvent.ACTION_UP -> {
resetDirtyRect(eventX, eventY)
addPoint(getNewPoint(eventX, eventY))
nodeCountX += (eventX - downX).absoluteValue
nodeCountY += (eventY - downY).absoluteValue
parent.requestDisallowInterceptTouchEvent(true)
}
else -> return false
}
//invalidate();
invalidate(
(mDirtyRect.left - mMaxWidth).toInt(),
(mDirtyRect.top - mMaxWidth).toInt(),
(mDirtyRect.right + mMaxWidth).toInt(),
(mDirtyRect.bottom + mMaxWidth).toInt()
)
return true
}
override fun onDraw(canvas: Canvas) {
if (mSignatureBitmap != null) {
canvas.drawBitmap(mSignatureBitmap!!, 0f, 0f, mPaint)
}
}
fun setOnSignedListener(listener: OnSignedListener?) {
mOnSignedListener = listener
}
var isEmpty: Boolean
get() = mIsEmpty
private set(newValue) {
mIsEmpty = newValue
if (mOnSignedListener != null) {
if (mIsEmpty) {
mOnSignedListener!!.onClear()
} else {
mOnSignedListener!!.onSigned()
}
}
}
val signatureSvg: String
get() {
val width = transparentSignatureBitmap!!.width
val height = transparentSignatureBitmap!!.height
return mSvgBuilder.build(width, height)
}
fun findSignatureBitmap(signature: Bitmap?): Bitmap? {
val originalBitmap = transparentSignatureBitmap
val whiteBgBitmap = Bitmap.createBitmap(
originalBitmap!!.width, originalBitmap.height, Bitmap.Config.ARGB_8888
)
val canvas = Canvas(whiteBgBitmap)
canvas.drawColor(Color.WHITE)
canvas.drawBitmap(originalBitmap, 0f, 0f, null)
return whiteBgBitmap
}
fun setTheSignature(signature: Bitmap) {
// View was laid out...
if (ViewCompat.isLaidOut(this)) {
clearView()
ensureSignatureBitmap()
val tempSrc = RectF()
val tempDst = RectF()
val dWidth = signature!!.width
val dHeight = signature.height
val vWidth = width
val vHeight = height
// Generate the required transform.
tempSrc[0f, 0f, dWidth.toFloat()] = dHeight.toFloat()
tempDst[0f, 0f, vWidth.toFloat()] = vHeight.toFloat()
val drawMatrix = Matrix()
drawMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER)
val canvas = Canvas(mSignatureBitmap!!)
canvas.drawBitmap(signature, drawMatrix, null)
isEmpty = false
invalidate()
} else {
viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
// Remove layout listener...
ViewTreeObserverCompat.removeOnGlobalLayoutListener(viewTreeObserver, this)
// Signature bitmap...
setTheSignature(signature)
}
})
}
}
val transparentSignatureBitmap: Bitmap?
get() {
ensureSignatureBitmap()
return mSignatureBitmap
}
fun getTransparentSignatureBitmap(trimBlankSpace: Boolean): Bitmap? {
if (!trimBlankSpace) {
return transparentSignatureBitmap
}
ensureSignatureBitmap()
val imgHeight = mSignatureBitmap!!.height
val imgWidth = mSignatureBitmap!!.width
val backgroundColor = Color.TRANSPARENT
var xMin = Int.MAX_VALUE
var xMax = Int.MIN_VALUE
var yMin = Int.MAX_VALUE
var yMax = Int.MIN_VALUE
var foundPixel = false
// Find xMin
for (x in 0 until imgWidth) {
var stop = false
for (y in 0 until imgHeight) {
if (mSignatureBitmap!!.getPixel(x, y) != backgroundColor) {
xMin = x
stop = true
foundPixel = true
break
}
}
if (stop) break
}
// Image is empty...
if (!foundPixel) return null
// Find yMin
for (y in 0 until imgHeight) {
var stop = false
for (x in xMin until imgWidth) {
if (mSignatureBitmap!!.getPixel(x, y) != backgroundColor) {
yMin = y
stop = true
break
}
}
if (stop) break
}
// Find xMax
for (x in imgWidth - 1 downTo xMin) {
var stop = false
for (y in yMin until imgHeight) {
if (mSignatureBitmap!!.getPixel(x, y) != backgroundColor) {
xMax = x
stop = true
break
}
}
if (stop) break
}
// Find yMax
for (y in imgHeight - 1 downTo yMin) {
var stop = false
for (x in xMin..xMax) {
if (mSignatureBitmap!!.getPixel(x, y) != backgroundColor) {
yMax = y
stop = true
break
}
}
if (stop) break
}
return Bitmap.createBitmap(mSignatureBitmap!!, xMin, yMin, xMax - xMin, yMax - yMin)
}
private fun onDoubleClick(): Boolean {
if (mClearOnDoubleClick) {
this.clearView()
return true
}
return false
}
private fun getNewPoint(x: Float, y: Float): TimedPoint {
val mCacheSize = mPointsCache.size
val timedPoint = if (mCacheSize == 0) {
// Cache is empty, create a new point
TimedPoint()
} else {
// Get point from cache
mPointsCache.removeAt(mCacheSize - 1)
}
return timedPoint.set(x, y)
}
private fun recyclePoint(point: TimedPoint) {
mPointsCache.add(point)
}
private fun addPoint(newPoint: TimedPoint) {
mPoints!!.add(newPoint)
val pointsCount = mPoints!!.size
if (pointsCount > 3) {
var tmp = calculateCurveControlPoints(mPoints!![0], mPoints!![1], mPoints!![2])
val c2 = tmp.c2
recyclePoint(tmp.c1)
tmp = calculateCurveControlPoints(mPoints!![1], mPoints!![2], mPoints!![3])
val c3 = tmp.c1
recyclePoint(tmp.c2)
val curve = mBezierCached.set(mPoints!![1], c2, c3, mPoints!![2])
val startPoint = curve.startPoint
val endPoint = curve.endPoint
var velocity = endPoint.velocityFrom(startPoint)
velocity = if (java.lang.Float.isNaN(velocity)) 0.0f else velocity
velocity = (mVelocityFilterWeight * velocity
+ (1 - mVelocityFilterWeight) * mLastVelocity)
// The new width is a function of the velocity. Higher velocities
// correspond to thinner strokes.
val newWidth = strokeWidth(velocity)
// The Bezier's width starts out as last curve's final width, and
// gradually changes to the stroke width just calculated. The new
// width calculation is based on the velocity between the Bezier's
// start and end mPoints.
addBezier(curve, mLastWidth, newWidth)
mLastVelocity = velocity
mLastWidth = newWidth
// Remove the first element from the list,
// so that we always have no more than 4 mPoints in mPoints array.
recyclePoint(mPoints!!.removeAt(0))
recyclePoint(c2)
recyclePoint(c3)
} else if (pointsCount == 1) {
// To reduce the initial lag make it work with 3 mPoints
// by duplicating the first point
val firstPoint = mPoints!![0]
mPoints!!.add(getNewPoint(firstPoint.x, firstPoint.y))
}
this.mHasEditState = true
}
private fun addBezier(curve: Bezier, startWidth: Float, endWidth: Float) {
mSvgBuilder.append(curve, (startWidth + endWidth) / 2)
ensureSignatureBitmap()
val originalWidth = mPaint.strokeWidth
val widthDelta = endWidth - startWidth
val drawSteps = ceil(curve.length())
var i = 0
while (i < drawSteps) {
// Calculate the Bezier (x, y) coordinate for this step.
val t = (i.toFloat()) / drawSteps
val tt = t * t
val ttt = tt * t
val u = 1 - t
val uu = u * u
val uuu = uu * u
var x = uuu * curve.startPoint.x
x += 3 * uu * t * curve.control1.x
x += 3 * u * tt * curve.control2.x
x += ttt * curve.endPoint.x
var y = uuu * curve.startPoint.y
y += 3 * uu * t * curve.control1.y
y += 3 * u * tt * curve.control2.y
y += ttt * curve.endPoint.y
// Set the incremental stroke width and draw.
mPaint.strokeWidth = startWidth + ttt * widthDelta
mSignatureBitmapCanvas!!.drawPoint(x, y, mPaint)
expandDirtyRect(x, y)
i++
}
mPaint.strokeWidth = originalWidth
}
private fun calculateCurveControlPoints(
s1: TimedPoint,
s2: TimedPoint,
s3: TimedPoint
): ControlTimedPoints {
val dx1 = s1.x - s2.x
val dy1 = s1.y - s2.y
val dx2 = s2.x - s3.x
val dy2 = s2.y - s3.y
val m1X = (s1.x + s2.x) / 2.0f
val m1Y = (s1.y + s2.y) / 2.0f
val m2X = (s2.x + s3.x) / 2.0f
val m2Y = (s2.y + s3.y) / 2.0f
val l1 = sqrt((dx1 * dx1 + dy1 * dy1).toDouble()).toFloat()
val l2 = sqrt((dx2 * dx2 + dy2 * dy2).toDouble()).toFloat()
val dxm = (m1X - m2X)
val dym = (m1Y - m2Y)
var k = l2 / (l1 + l2)
if (java.lang.Float.isNaN(k)) k = 0.0f
val cmX = m2X + dxm * k
val cmY = m2Y + dym * k
val tx = s2.x - cmX
val ty = s2.y - cmY
return mControlTimedPointsCached.set(
getNewPoint(m1X + tx, m1Y + ty),
getNewPoint(m2X + tx, m2Y + ty)
)
}
private fun strokeWidth(velocity: Float): Float {
return max((mMaxWidth / (velocity + 1)).toDouble(), mMinWidth.toDouble())
.toFloat()
}
/**
* Called when replaying history to ensure the dirty region includes all
* mPoints.
*
* @param historicalX the previous x coordinate.
* @param historicalY the previous y coordinate.
*/
private fun expandDirtyRect(historicalX: Float, historicalY: Float) {
if (historicalX < mDirtyRect.left) {
mDirtyRect.left = historicalX
} else if (historicalX > mDirtyRect.right) {
mDirtyRect.right = historicalX
}
if (historicalY < mDirtyRect.top) {
mDirtyRect.top = historicalY
} else if (historicalY > mDirtyRect.bottom) {
mDirtyRect.bottom = historicalY
}
}
/**
* Resets the dirty region when the motion event occurs.
*
* @param eventX the event x coordinate.
* @param eventY the event y coordinate.
*/
private fun resetDirtyRect(eventX: Float, eventY: Float) {
// The mLastTouchX and mLastTouchY were set when the ACTION_DOWN motion event occurred.
mDirtyRect.left = min(mLastTouchX.toDouble(), eventX.toDouble()).toFloat()
mDirtyRect.right = max(mLastTouchX.toDouble(), eventX.toDouble()).toFloat()
mDirtyRect.top = min(mLastTouchY.toDouble(), eventY.toDouble()).toFloat()
mDirtyRect.bottom = max(mLastTouchY.toDouble(), eventY.toDouble()).toFloat()
}
private fun ensureSignatureBitmap() {
if (mSignatureBitmap == null) {
mSignatureBitmap = Bitmap.createBitmap(
width, height,
Bitmap.Config.ARGB_8888
)
mSignatureBitmapCanvas = Canvas(mSignatureBitmap!!)
}
}
private fun convertDpToPx(dp: Float): Int {
return Math.round(context.resources.displayMetrics.density * dp)
}
val points: List<TimedPoint>?
get() = mPoints
companion object {
private val TAG: String = SignaturePad::class.java.name
}
/*===================*/
/**
* 移除文件背景
*
* @param bitmap 图片文件
* @return
*/
fun removeBackground(bitmap: Bitmap): Bitmap {
val portraitWidth = bitmap.width
val portraitHeight = bitmap.height
val colors = IntArray(portraitWidth * portraitHeight)
bitmap.getPixels(
colors,
0,
portraitWidth,
0,
0,
portraitWidth,
portraitHeight
) // 获得图片的ARGB值
for (i in colors.indices) {
val a = Color.alpha(colors[i])
val r = Color.red(colors[i])
val g = Color.green(colors[i])
val b = Color.blue(colors[i])
if (r > 240 && g > 240 && b > 240) {
colors[i] = 0x00FFFFFF
}
}
return Bitmap.createBitmap(
colors,
0,
portraitWidth,
portraitWidth,
portraitHeight,
Bitmap.Config.ARGB_4444
)
}
/**
* 同步获取文件
*/
fun getFile(): File? {
return mBitmapSavedState?.let { toFile(it, null) } ?: run {
transparentSignatureBitmap?.let { toFile(it, listener = null) }
}
}
/**
* 异步获取文件
*
* @param listener
*/
fun getFile(listener: OnSignedListener?) {
mBitmapSavedState?.let { toFile(it, listener) } ?: run {
transparentSignatureBitmap?.let { toFile(it, listener) }
}
}
/**
* @param bitmap 位图
* @return 转换Bitmap为文件
*/
fun toFile(bitmap: Bitmap, listener: OnSignedListener?): File {
var bitmap = bitmap
bitmap = removeBackground(bitmap)
val format = SimpleDateFormat("yyyyMMddHHmmss")
val dirFile = File(context.externalCacheDir, dir)
if (!dirFile.exists()) {
dirFile.mkdirs()
}
val file = File(dirFile, ("IMS_" + format.format(Date())) + ".png")
val stream: BufferedOutputStream
try {
stream = BufferedOutputStream(FileOutputStream(file))
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
stream.flush()
stream.close()
listener?.onSignatureFile(file)
} catch (e: IOException) {
e.printStackTrace()
}
return file
}
interface OnSignedListener {
fun onStartSigning()
fun onSigned()
fun onClear()
fun onSignatureFile(file: File)
}
}
public class Bezier {
public TimedPoint startPoint;
public TimedPoint control1;
public TimedPoint control2;
public TimedPoint endPoint;
public Bezier set(TimedPoint startPoint, TimedPoint control1,
TimedPoint control2, TimedPoint endPoint) {
this.startPoint = startPoint;
this.control1 = control1;
this.control2 = control2;
this.endPoint = endPoint;
return this;
}
public float length() {
int steps = 10;
float length = 0;
double cx, cy, px = 0, py = 0, xDiff, yDiff;
for (int i = 0; i <= steps; i++) {
float t = (float) i / steps;
cx = point(t, this.startPoint.getX(), this.control1.getX(),
this.control2.getX(), this.endPoint.getX());
cy = point(t, this.startPoint.getY(), this.control1.getY(),
this.control2.getY(), this.endPoint.getY());
if (i > 0) {
xDiff = cx - px;
yDiff = cy - py;
length += Math.sqrt(xDiff * xDiff + yDiff * yDiff);
}
px = cx;
py = cy;
}
return length;
}
public double point(float t, float start, float c1, float c2, float end) {
return start * (1.0 - t) * (1.0 - t) * (1.0 - t)
+ 3.0 * c1 * (1.0 - t) * (1.0 - t) * t
+ 3.0 * c2 * (1.0 - t) * t * t
+ end * t * t * t;
}
}
public class ControlTimedPoints {
public TimedPoint c1;
public TimedPoint c2;
public ControlTimedPoints set(TimedPoint c1, TimedPoint c2) {
this.c1 = c1;
this.c2 = c2;
return this;
}
}
public class SvgBuilder {
private final StringBuilder mSvgPathsBuilder = new StringBuilder();
private SvgPathBuilder mCurrentPathBuilder = null;
public SvgBuilder() {
}
public void clear() {
mSvgPathsBuilder.setLength(0);
mCurrentPathBuilder = null;
}
public String build(final int width, final int height) {
if (isPathStarted()) {
appendCurrentPath();
}
return (new StringBuilder())
.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n")
.append("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.2\" baseProfile=\"tiny\" ")
.append("height=\"")
.append(height)
.append("\" ")
.append("width=\"")
.append(width)
.append("\" ")
.append("viewBox=\"")
.append(0)
.append(" ")
.append(0)
.append(" ")
.append(width)
.append(" ")
.append(height)
.append("\">")
.append("<g ")
.append("stroke-linejoin=\"round\" ")
.append("stroke-linecap=\"round\" ")
.append("fill=\"none\" ")
.append("stroke=\"black\"")
.append(">")
.append(mSvgPathsBuilder)
.append("</g>")
.append("</svg>")
.toString();
}
public SvgBuilder append(final Bezier curve, final float strokeWidth) {
final Integer roundedStrokeWidth = Math.round(strokeWidth);
final SvgPoint curveStartSvgPoint = new SvgPoint(curve.startPoint);
final SvgPoint curveControlSvgPoint1 = new SvgPoint(curve.control1);
final SvgPoint curveControlSvgPoint2 = new SvgPoint(curve.control2);
final SvgPoint curveEndSvgPoint = new SvgPoint(curve.endPoint);
if (!isPathStarted()) {
startNewPath(roundedStrokeWidth, curveStartSvgPoint);
}
if (!curveStartSvgPoint.equals(mCurrentPathBuilder.getLastPoint())
|| !roundedStrokeWidth.equals(mCurrentPathBuilder.getStrokeWidth())) {
appendCurrentPath();
startNewPath(roundedStrokeWidth, curveStartSvgPoint);
}
mCurrentPathBuilder.append(curveControlSvgPoint1, curveControlSvgPoint2, curveEndSvgPoint);
return this;
}
private void startNewPath(Integer roundedStrokeWidth, SvgPoint curveStartSvgPoint) {
mCurrentPathBuilder = new SvgPathBuilder(curveStartSvgPoint, roundedStrokeWidth);
}
private void appendCurrentPath() {
mSvgPathsBuilder.append(mCurrentPathBuilder);
}
private boolean isPathStarted() {
return mCurrentPathBuilder != null;
}
}
import kotlin.math.pow
import kotlin.math.sqrt
/*
* @Description: TimePoint
* @Version: v1.0
* @Author: Lani.wong
* @Date: 2024-12-09 15:49
*/
class TimedPoint {
var x: Float = 0f
var y: Float = 0f
var timestamp: Long = 0
fun set(x: Float, y: Float): TimedPoint {
this.x = x
this.y = y
this.timestamp = System.currentTimeMillis()
return this
}
fun velocityFrom(start: TimedPoint): Float {
var diff = this.timestamp - start.timestamp
if (diff <= 0) {
diff = 1
}
var velocity = distanceTo(start) / diff
if (java.lang.Float.isInfinite(velocity) || java.lang.Float.isNaN(velocity)) {
velocity = 0f
}
return velocity
}
fun distanceTo(point: TimedPoint): Float {
return sqrt((point.x - this.x).pow(2.0f) + (point.y - this.y).pow(2.0f))
.toFloat()
}
}
acitivity使用
<com.purui.mobile.ui.signature.view.SignaturePad
android:id="@+id/sign2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="15dp"
android:background="#fff"
android:elevation="2dp"
/>
判断笔画像素点数:
nodeCountX,横向像素点, nodeCountY,纵向像素点。用于校验笔画是否太少。
if((binding?.sign2?.nodeCountX)!! <80f || binding?.sign2?.nodeCountY!! <80f){
Toast.makeText(this,"笔画过少,请重新签名", Toast.LENGTH_SHORT).show()
return@cm
}
获取签名位图
binding?.sign2?.findSignatureBitmap(null)
清空签名
binding?.sign2?.clear()