Core Android

Android Capture Signature using Canvas and Save

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.

Get Solution Code

Leave a Reply

  Subscribe  
Notify of