先上效果图:
实现
自定义一个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 stateprivate var mPoints: MutableList<TimedPoint>? = nullprivate var mIsEmpty = falseprivate var mHasEditState: Boolean? = nullprivate var mLastTouchX = 0fprivate var mLastTouchY = 0fprivate var mLastVelocity = 0fprivate var mLastWidth = 0fprivate val mDirtyRect: RectFprivate var mBitmapSavedState: Bitmap? = nullprivate val mSvgBuilder = SvgBuilder()// Cacheprivate val mPointsCache: MutableList<TimedPoint> = ArrayList()private val mControlTimedPointsCached = ControlTimedPoints()private val mBezierCached = Bezier()//Configurable parametersprivate var mMinWidth = 0private var mMaxWidth = 0private var mVelocityFilterWeight = 0fprivate var mOnSignedListener: OnSignedListener? = nullprivate var mClearOnDoubleClick = false//Double click detectorprivate val mGestureDetector: GestureDetector//Default attribute valuesprivate val DEFAULT_ATTR_PEN_MIN_WIDTH_PX = 3private val DEFAULT_ATTR_PEN_MAX_WIDTH_PX = 7private val DEFAULT_ATTR_PEN_COLOR = Color.BLACKprivate val DEFAULT_ATTR_VELOCITY_FILTER_WEIGHT = 0.9fprivate val DEFAULT_ATTR_CLEAR_ON_DOUBLE_CLICK = falseprivate val mPaint = Paint()private var mSignatureBitmap: Bitmap? = nullprivate 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 parameterstry {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 parametersmPaint.isAntiAlias = truemPaint.style = Paint.Style.STROKEmPaint.strokeCap = Paint.Cap.ROUNDmPaint.strokeJoin = Paint.Join.ROUND//Dirty rectangle to update only the changed portion of the viewmDirtyRect = 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? = stateif (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 = falsesuper.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 = 0fvar nodeCountY: Float = 0fvar downX: Float = 0fvar downY: Float = 0ffun clearView() {nodeCountX = 0fnodeCountY = 0fdownY = 0fdownY = 0fmSvgBuilder.clear()mPoints = ArrayList()mLastVelocity = 0fmLastWidth = (mMinWidth + mMaxWidth) / 2fif (mSignatureBitmap != null) {mSignatureBitmap = nullensureSignatureBitmap()}isEmpty = trueinvalidate()}fun clear() {this.clearView()this.mHasEditState = true}override fun onTouchEvent(event: MotionEvent): Boolean {if (!isEnabled) return falseval eventX = event.xval eventY = event.ywhen (event.action) {MotionEvent.ACTION_DOWN -> {parent.requestDisallowInterceptTouchEvent(true)mPoints!!.clear()if (mGestureDetector.onTouchEvent(event)) {// break} else {mLastTouchX = eventXmLastTouchY = eventYdownX = eventXdownY = eventYaddPoint(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).absoluteValuenodeCountY += (eventY - downY).absoluteValueparent.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: Booleanget() = mIsEmptyprivate set(newValue) {mIsEmpty = newValueif (mOnSignedListener != null) {if (mIsEmpty) {mOnSignedListener!!.onClear()} else {mOnSignedListener!!.onSigned()}}}val signatureSvg: Stringget() {val width = transparentSignatureBitmap!!.widthval height = transparentSignatureBitmap!!.heightreturn mSvgBuilder.build(width, height)}fun findSignatureBitmap(signature: Bitmap?): Bitmap? {val originalBitmap = transparentSignatureBitmapval 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!!.widthval dHeight = signature.heightval vWidth = widthval 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 = falseinvalidate()} 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!!.heightval imgWidth = mSignatureBitmap!!.widthval backgroundColor = Color.TRANSPARENTvar xMin = Int.MAX_VALUEvar xMax = Int.MIN_VALUEvar yMin = Int.MAX_VALUEvar yMax = Int.MIN_VALUEvar foundPixel = false// Find xMinfor (x in 0 until imgWidth) {var stop = falsefor (y in 0 until imgHeight) {if (mSignatureBitmap!!.getPixel(x, y) != backgroundColor) {xMin = xstop = truefoundPixel = truebreak}}if (stop) break}// Image is empty...if (!foundPixel) return null// Find yMinfor (y in 0 until imgHeight) {var stop = falsefor (x in xMin until imgWidth) {if (mSignatureBitmap!!.getPixel(x, y) != backgroundColor) {yMin = ystop = truebreak}}if (stop) break}// Find xMaxfor (x in imgWidth - 1 downTo xMin) {var stop = falsefor (y in yMin until imgHeight) {if (mSignatureBitmap!!.getPixel(x, y) != backgroundColor) {xMax = xstop = truebreak}}if (stop) break}// Find yMaxfor (y in imgHeight - 1 downTo yMin) {var stop = falsefor (x in xMin..xMax) {if (mSignatureBitmap!!.getPixel(x, y) != backgroundColor) {yMax = ystop = truebreak}}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.sizeval timedPoint = if (mCacheSize == 0) {// Cache is empty, create a new pointTimedPoint()} else {// Get point from cachemPointsCache.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!!.sizeif (pointsCount > 3) {var tmp = calculateCurveControlPoints(mPoints!![0], mPoints!![1], mPoints!![2])val c2 = tmp.c2recyclePoint(tmp.c1)tmp = calculateCurveControlPoints(mPoints!![1], mPoints!![2], mPoints!![3])val c3 = tmp.c1recyclePoint(tmp.c2)val curve = mBezierCached.set(mPoints!![1], c2, c3, mPoints!![2])val startPoint = curve.startPointval endPoint = curve.endPointvar velocity = endPoint.velocityFrom(startPoint)velocity = if (java.lang.Float.isNaN(velocity)) 0.0f else velocityvelocity = (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 = velocitymLastWidth = 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 pointval 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.strokeWidthval widthDelta = endWidth - startWidthval drawSteps = ceil(curve.length())var i = 0while (i < drawSteps) {// Calculate the Bezier (x, y) coordinate for this step.val t = (i.toFloat()) / drawStepsval tt = t * tval ttt = tt * tval u = 1 - tval uu = u * uval uuu = uu * uvar x = uuu * curve.startPoint.xx += 3 * uu * t * curve.control1.xx += 3 * u * tt * curve.control2.xx += ttt * curve.endPoint.xvar y = uuu * curve.startPoint.yy += 3 * uu * t * curve.control1.yy += 3 * u * tt * curve.control2.yy += ttt * curve.endPoint.y// Set the incremental stroke width and draw.mPaint.strokeWidth = startWidth + ttt * widthDeltamSignatureBitmapCanvas!!.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.xval dy1 = s1.y - s2.yval dx2 = s2.x - s3.xval dy2 = s2.y - s3.yval m1X = (s1.x + s2.x) / 2.0fval m1Y = (s1.y + s2.y) / 2.0fval m2X = (s2.x + s3.x) / 2.0fval m2Y = (s2.y + s3.y) / 2.0fval 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.0fval cmX = m2X + dxm * kval cmY = m2Y + dym * kval tx = s2.x - cmXval ty = s2.y - cmYreturn 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() = mPointscompanion object {private val TAG: String = SignaturePad::class.java.name}/*===================*//*** 移除文件背景** @param bitmap 图片文件* @return*/fun removeBackground(bitmap: Bitmap): Bitmap {val portraitWidth = bitmap.widthval portraitHeight = bitmap.heightval 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 = bitmapbitmap = 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: BufferedOutputStreamtry {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 = 0fvar y: Float = 0fvar timestamp: Long = 0fun set(x: Float, y: Float): TimedPoint {this.x = xthis.y = ythis.timestamp = System.currentTimeMillis()return this}fun velocityFrom(start: TimedPoint): Float {var diff = this.timestamp - start.timestampif (diff <= 0) {diff = 1}var velocity = distanceTo(start) / diffif (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.SignaturePadandroid: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()