Android & Kotlin

Android Capture Signature using Canvas and Save

Pinterest LinkedIn Tumblr

In this blog, I’ll show you how to capture signature using canvas and save it. For doing that we’ll create a custom SignatureView and use it in our sample app. So let’s get started.

1. Create an android project and add resource file in the string.xml
<resources>
    <string name="app_name">Capture Signature</string>
    <string name="ok">Ok</string>
    <string name="clear">Clear</string>
    <string name="cancel">Cancel</string>
</resources>
    <style name="Dialog.App" parent="AppTheme">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowFullscreen">false</item>
        <item name="android:windowIsFloating">false</item>

    </style>
2. Write attribute for SignatureView

Create a new file inside res=>value=>attrs.xml, and paste below code

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SignatureView">
        <attr name="penMinWidth" format="dimension" />
        <attr name="penMaxWidth" format="dimension" />
        <attr name="penColor" format="color" />
        <attr name="velocityFilterWeight" format="float" />
        <attr name="clearOnDoubleClick" format="boolean" />
    </declare-styleable>
</resources>
3. Create a Signature View.

In the src folder create a new java class named is SignatureView.java. let’s copy all code and paste into the own class. after that, we have to create a utils file that I explain below.

package com.capturesignature.view

import android.content.Context
import android.content.res.Resources.NotFoundException
import android.graphics.*
import android.graphics.Bitmap.Config.ARGB_8888
import android.graphics.Matrix.ScaleToFit.CENTER
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import com.capturesignature.R.styleable
import com.capturesignature.utils.Bezier
import com.capturesignature.utils.ControlTimedPoints
import com.capturesignature.utils.TimedPoint
import com.capturesignature.view.ViewCompat.isLaidOut
import java.util.*
import kotlin.math.max
import kotlin.math.roundToInt

class SignatureView(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

    // Cache
    private val mPointsCache: MutableList<TimedPoint> = ArrayList()
    private val mControlTimedPointsCached: ControlTimedPoints =
        ControlTimedPoints()
    private val mBezierCached: Bezier =
        Bezier()
    //Configurable parameters
    private var mMinWidth = 0
    private var mMaxWidth = 0
    private var mVelocityFilterWeight = 0f
    private var mOnSignedListener: OnSignedListener? = null
    private var mClearOnDoubleClick = false
    //Click values
    private var mFirstClick: Long = 0
    private var mCountClick = 0
    //Default attribute values
    private val DEFAULT_ATTR_PEN_MIN_WIDTH_PX = 2
    private val DEFAULT_ATTR_PEN_MAX_WIDTH_PX = 3
    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
    override fun onSaveInstanceState(): Parcelable? {
        val bundle = Bundle()
        bundle.putParcelable("superState", super.onSaveInstanceState())
        if (mHasEditState == null || mHasEditState!!) {
            mBitmapSavedState = getTransparentSignatureBitmap()
        }
        bundle.putParcelable("signatureBitmap", mBitmapSavedState)
        return bundle
    }

    override fun onRestoreInstanceState(state: Parcelable) {
        var state: Parcelable? = state
        if (state is Bundle) {
            val bundle = state
            setSignatureBitmap(bundle.getParcelable<Parcelable>("signatureBitmap") as Bitmap)
            mBitmapSavedState = bundle.getParcelable("signatureBitmap")
            state = bundle.getParcelable("superState")
        }
        mHasEditState = false
        super.onRestoreInstanceState(state)
    }

    /**
     * Set the pen color from a given resource.
     * If the resource is not found, [Color.BLACK] is assumed.
     *
     * @param colorRes the color resource.
     */
    fun setPenColorRes(colorRes: Int) {
        try {
            setPenColor(resources.getColor(colorRes))
        } catch (ex: NotFoundException) {
            setPenColor(Color.parseColor("#000000"))
        }
    }

    /**
     * Set the pen color from a given color.
     *
     * @param color the color.
     */
    private 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)
    }

    /**
     * Set the maximum width of the stroke in pixel.
     *
     * @param maxWidth the width in dp.
     */
    fun setMaxWidth(maxWidth: Float) {
        mMaxWidth = convertDpToPx(maxWidth)
    }

    /**
     * Set the velocity filter weight.
     *
     * @param velocityFilterWeight the weight.
     */
    fun setVelocityFilterWeight(velocityFilterWeight: Float) {
        mVelocityFilterWeight = velocityFilterWeight
    }

    private fun clearView() {
        mPoints = ArrayList()
        mLastVelocity = 0f
        mLastWidth = (mMinWidth + mMaxWidth) / 2.toFloat()
        if (mSignatureBitmap != null) {
            mSignatureBitmap = null
            ensureSignatureBitmap()
        }
        setIsEmpty(true)
        invalidate()
    }

    fun clear() {
        clearView()
        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 (isDoubleClick()) {
                    return false
                }
                mLastTouchX = eventX
                mLastTouchY = eventY
                addPoint(getNewPoint(eventX, eventY))
                if (mOnSignedListener != null) mOnSignedListener!!.onStartSigning()
                resetDirtyRect(eventX, eventY)
                addPoint(getNewPoint(eventX, eventY))
                setIsEmpty(false)
            }
            MotionEvent.ACTION_MOVE -> {
                resetDirtyRect(eventX, eventY)
                addPoint(getNewPoint(eventX, eventY))
                setIsEmpty(false)
            }
            MotionEvent.ACTION_UP -> {
                resetDirtyRect(eventX, eventY)
                addPoint(getNewPoint(eventX, eventY))
                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
    }

    private fun setIsEmpty(newValue: Boolean) {
        mIsEmpty = newValue
        if (mOnSignedListener != null) {
            if (mIsEmpty) {
                mOnSignedListener!!.onClear()
            } else {
                mOnSignedListener!!.onSigned()
            }
        }
    }

    fun getSignatureBitmap(): Bitmap {
        val originalBitmap = getTransparentSignatureBitmap()
        val whiteBgBitmap = Bitmap.createBitmap(
            originalBitmap!!.width, originalBitmap.height, ARGB_8888
        )
        val canvas = Canvas(whiteBgBitmap)
        canvas.drawColor(Color.WHITE)
        canvas.drawBitmap(originalBitmap, 0f, 0f, null)
        return whiteBgBitmap
    }

    fun setSignatureBitmap(signature: Bitmap) {
        if (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, CENTER)
            val canvas = Canvas(mSignatureBitmap!!)
            canvas.drawBitmap(signature, drawMatrix, null)
            setIsEmpty(false)
            invalidate()
        } else {
            viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
                override fun onGlobalLayout() {
                    // Remove layout listener...
                    ViewTreeObserverCompat.removeOnGlobalLayoutListener(viewTreeObserver, this)
                    // Signature bitmap...
                    setSignatureBitmap(signature)
                }
            })

        }

    }

    private fun getTransparentSignatureBitmap(): Bitmap? {
        ensureSignatureBitmap()
        return mSignatureBitmap
    }

    private fun getTransparentSignatureBitmap(trimBlankSpace: Boolean): Bitmap? {
        if (!trimBlankSpace) {
            return getTransparentSignatureBitmap()
        }
        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 isDoubleClick(): Boolean {
        if (mClearOnDoubleClick) {
            if (mFirstClick != 0L && System.currentTimeMillis() - mFirstClick > DOUBLE_CLICK_DELAY_MS) {
                mCountClick = 0
            }
            mCountClick++
            if (mCountClick == 1) {
                mFirstClick = System.currentTimeMillis()
            } else if (mCountClick == 2) {
                val lastClick = System.currentTimeMillis()
                if (lastClick - mFirstClick < DOUBLE_CLICK_DELAY_MS) {
                    clearView()
                    return true
                }
            }
        }
        return false
    }

    private fun getNewPoint(
        x: Float,
        y: Float
    ): TimedPoint {
        val mCacheSize = mPointsCache.size
        val timedPoint: TimedPoint
        if (mCacheSize == 0) { // Cache is empty, create a new point
            timedPoint = TimedPoint()
        } else { // Get point from cache
            timedPoint = 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: ControlTimedPoints =
                calculateCurveControlPoints(mPoints!![0], mPoints!![1], mPoints!![2])
            val c2: TimedPoint = tmp.c2
            recyclePoint(tmp.c1)
            tmp = calculateCurveControlPoints(mPoints!![1], mPoints!![2], mPoints!![3])
            val c3: TimedPoint = tmp.c1
            recyclePoint(tmp.c2)
            val curve: Bezier = mBezierCached.set(mPoints!![1], c2, c3, mPoints!![2])
            val startPoint: TimedPoint = curve.startPoint
            val endPoint: TimedPoint = curve.endPoint
            var velocity: Float = 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)

            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: TimedPoint = mPoints!![0]
            mPoints!!.add(getNewPoint(firstPoint.x, firstPoint.y))
        }
        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 = Math.ceil(curve.length().toDouble())
            .toFloat()
        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: Float = 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: Float = 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: Float = s1.x - s2.x
        val dy1: Float = s1.y - s2.y
        val dx2: Float = s2.x - s3.x
        val dy2: Float = s2.y - s3.y
        val m1X: Float = (s1.x + s2.x) / 2.0f
        val m1Y: Float = (s1.y + s2.y) / 2.0f
        val m2X: Float = (s2.x + s3.x) / 2.0f
        val m2Y: Float = (s2.y + s3.y) / 2.0f
        val l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1.toDouble())
            .toFloat()
        val l2 = Math.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: Float = s2.x - cmX
        val ty: Float = 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), mMinWidth.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 = Math.min(mLastTouchX, eventX)
        mDirtyRect.right = Math.max(mLastTouchX, eventX)
        mDirtyRect.top = Math.min(mLastTouchY, eventY)
        mDirtyRect.bottom = Math.max(mLastTouchY, eventY)
    }

    private fun ensureSignatureBitmap() {
        if (mSignatureBitmap == null) {
            mSignatureBitmap = Bitmap.createBitmap(
                width, height,
                ARGB_8888
            )
            mSignatureBitmapCanvas = Canvas(mSignatureBitmap!!)
        }
    }

    private fun convertDpToPx(dp: Float): Int {
        return (context.resources.displayMetrics.density * dp).roundToInt()
    }

    interface OnSignedListener {
        fun onStartSigning()
        fun onSigned()
        fun onClear()
    }

    private fun getPoints(): List<TimedPoint?>? {
        return mPoints
    }

    companion object {
        private const val DOUBLE_CLICK_DELAY_MS = 200
    }

    init {
        val a = context.theme.obtainStyledAttributes(
            attrs,
            styleable.SignatureView,
            0, 0
        )
        //Configurable parameters
        try {
            mMinWidth = a.getDimensionPixelSize(
                styleable.SignatureView_penMinWidth,
                convertDpToPx(DEFAULT_ATTR_PEN_MIN_WIDTH_PX.toFloat())
            )
            mMaxWidth = a.getDimensionPixelSize(
                styleable.SignatureView_penMaxWidth,
                convertDpToPx(DEFAULT_ATTR_PEN_MAX_WIDTH_PX.toFloat())
            )
            mPaint.color = a.getColor(
                styleable.SignatureView_penColor, DEFAULT_ATTR_PEN_COLOR
            )
            mVelocityFilterWeight = a.getFloat(
                styleable.SignatureView_velocityFilterWeight,
                DEFAULT_ATTR_VELOCITY_FILTER_WEIGHT
            )
            mClearOnDoubleClick = a.getBoolean(
                styleable.SignatureView_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()
    }
}
4. Create a view utils file ViewCompat
package com.capturesignature.view

import android.os.Build
import android.view.View

object ViewCompat {

  fun isLaidOut(view: View): Boolean {
    return if (Build.VERSION.SDK_INT >= 19) {
      view.isLaidOut
    } else view.width > 0 && view.height > 0
  }
}
5. Create a view utils class named ViewTreeObserverCompat
package com.capturesignature.view

import android.annotation.SuppressLint
import android.os.Build
import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.OnGlobalLayoutListener

object ViewTreeObserverCompat {
  /**
   * Remove a previously installed global layout callback.
   * @param observer the view observer
   * @param victim the victim
   */
  @SuppressLint("NewApi")
  fun removeOnGlobalLayoutListener(
    observer: ViewTreeObserver,
    victim: OnGlobalLayoutListener?
  ) { // Future (API16+)...
    if (Build.VERSION.SDK_INT >= 16) {
      observer.removeOnGlobalLayoutListener(victim)
    } else {
      observer.removeGlobalOnLayoutListener(victim)
    }
  }
}
6. Create another package named is utils and paste 3 utilizes class.
Bezier.java
package com.capturesignature.utils

class Bezier {

  lateinit var startPoint: TimedPoint

  lateinit var control1: TimedPoint

  lateinit var control2: TimedPoint

  lateinit var endPoint: TimedPoint
  operator fun set(
    startPoint: TimedPoint,
    control1: TimedPoint,
    control2: TimedPoint,
    endPoint: TimedPoint
  ): Bezier {
    this.startPoint = startPoint
    this.control1 = control1
    this.control2 = control2
    this.endPoint = endPoint
    return this
  }

  fun length(): Float {
    val steps = 10
    var length = 0f
    var cx: Double
    var cy: Double
    var px = 0.0
    var py = 0.0
    var xDiff: Double
    var yDiff: Double
    for (i in 0..steps) {
      val t = i.toFloat() / steps
      cx = point(
          t, startPoint.x, control1.x,
          control2.x, endPoint.x
      )
      cy = point(
          t, startPoint.y, control1.y,
          control2.y, endPoint.y
      )
      if (i > 0) {
        xDiff = cx - px
        yDiff = cy - py
        length += Math.sqrt(xDiff * xDiff + yDiff * yDiff)
            .toFloat()
      }
      px = cx
      py = cy
    }
    return length
  }

  private fun point(
    t: Float,
    start: Float,
    c1: Float,
    c2: Float,
    end: Float
  ): Double {
    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
  }
}
ControlTimedPoints.java
package com.capturesignature.utils

class ControlTimedPoints {
    lateinit var c1: TimedPoint
    lateinit var c2: TimedPoint
    operator fun set(c1: TimedPoint, c2: TimedPoint): ControlTimedPoints {
        this.c1 = c1
        this.c2 = c2
        return this
    }
}
TimedPoint.java
package com.capturesignature.utils

class TimedPoint {

  var x = 0f
  var y = 0f
  var timestamp: Long = 0

  operator fun set(
    x: Float,
    y: Float
  ): TimedPoint {
    this.x = x
    this.y = y
    timestamp = System.currentTimeMillis()
    return this
  }

  fun velocityFrom(start: TimedPoint): Float {
    var diff = 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
  }

  private fun distanceTo(point: TimedPoint): Float {
    return Math.sqrt(
        Math.pow(
            point.x - x.toDouble(),
            2.0
        ) + Math.pow(point.y - y.toDouble(), 2.0)
    )
        .toFloat()
  }
}
7. Create a layout file for capture signature pad

In this layout file, we have three buttons with SignatureView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorWhite"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/buttonCancel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:background="@color/colorAccent"
        android:text="@string/cancel"
        android:textColor="@drawable/button_text_color"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/buttonClear"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/buttonClear"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:background="@color/colorAccent"
        android:text="@string/clear"
        android:textColor="@drawable/button_text_color"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/buttonOk"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/buttonCancel" />

    <Button
        android:id="@+id/buttonOk"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:background="@color/colorAccent"
        android:text="@string/ok"
        android:textColor="@drawable/button_text_color"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/buttonClear" />

    <com.capturesignature.view.SignatureView
        android:id="@+id/signatureView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="16dp"
        app:layout_constraintBottom_toTopOf="@+id/buttonClear"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
8. Create a dialog fragment SignatureDialogFragment
package com.capturesignature

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.capturesignature.view.SignatureView
import kotlinx.android.synthetic.main.fragment_signature_dialog.*

class SignatureDialogFragment(private val onSignedListener: OnSignedCaptureListener) :
    DialogFragment(),
    SignatureView.OnSignedListener {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        isCancelable = false
        return inflater.inflate(R.layout.fragment_signature_dialog, container, false)
    }

    override fun getTheme(): Int {
        return R.style.Dialog_App
    }


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        buttonCancel.setOnClickListener { dismiss() }
        buttonClear.setOnClickListener { signatureView.clear() }
        buttonOk.setOnClickListener {
            onSignedListener.onSignatureCaptured(signatureView.getSignatureBitmap(), "")
            dismiss()
        }
        signatureView.setOnSignedListener(this)

    }

    override fun onStartSigning() {

    }

    override fun onSigned() {
        buttonOk.isEnabled = true
        buttonClear.isEnabled = true
    }

    override fun onClear() {
        buttonClear.isEnabled = false
        buttonOk.isEnabled = false
    }
}
Open the MainActivity class and paste below code

Basically, we are creating an instance of SignatureDialogFragment. This fragment is written a signature image Bitmap in onSignatureCaptured() callback.

package com.capturesignature

import android.graphics.Bitmap
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*


class MainActivity : AppCompatActivity(), OnSignedCaptureListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        buttonShowDialog.setOnClickListener { showDialog() }

    }

    private fun showDialog() {
        val dialogFragment = SignatureDialogFragment(this)
        dialogFragment.show(supportFragmentManager, "signature")
    }

    override fun onSignatureCaptured(bitmap: Bitmap, fileUri: String) {
        imageView.setImageBitmap(bitmap)
    }
}

Conclusion

In this blog, we have learned the implementation of a capture signature using Canvas in Android. I hope it’s helpful for you. Thanks for reading this post, I would like to recommend the new way of performing background jobs using Coroutines

Get Solution Code

Write A Comment