当前位置: 首页 > article >正文

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()


http://www.kler.cn/a/452755.html

相关文章:

  • 服务器上加入SFTP------(小白篇 1)
  • linux系统上SQLPLUS的重“大”发现
  • 互联网视频云平台EasyDSS无人机推流直播技术如何助力野生动植物保护工作?
  • Odoo 免费开源 ERP:通过 JavaScript 创建对话框窗口的技术实践分享
  • 仿真中产生的simv文件
  • 如何查看pad的console输出,以便我们更好的进行调试,查看并了解实际可能的问题。
  • windows C#-编写复制构造函数
  • 掌握Go语言:配置环境变量、深入理解GOPATH和GOROOT(1)
  • Java中String类型的字符串转换成JSON对象和JSON字符串
  • [STM32] 串口通信 (十一)
  • 【落羽的落羽 C语言篇】数据存储简介
  • 车载网关性能 --- 缓存buffer划分要求
  • 109.【C语言】数据结构之求二叉树的高度
  • 探究人工智能在教育领域的应用——以大语言模型为例
  • 【JAVA高级篇教学】第五篇:OpenFeign 微服务调用注意事项
  • docker commit生成的镜像瘦身
  • 参数名在不同的SpringBoot版本中,处理方案不同
  • 深度学习笔记1:神经网络与模型训练过程
  • Java设计模式 —— 【结构型模式】享元模式(Flyweight Pattern) 详解
  • C++-----------数组
  • Linux复习2——管理文件系统1
  • 数据可视化期末复习-简答题
  • golang,多个proxy拉包的处理逻辑
  • MT6765核心板_MTK6765安卓核心板规格参数_联发科MTK模块开发
  • 结构化Prompt:让大模型更智能的秘诀
  • 保姆级教程Docker部署RabbitMQ镜像