In this article, we’re going to be exploring how to create an application that lets us capture a person’s signature, by drawing on the screen of the device.

This would be achieved by creating a Custom class and making use of a Canvas implementation in Kotlin.

Here’s the demo of our app

Pre-requisites

Android Studio 3.0 or later and a fair knowledge around it.

An Up-to-date Android Studio

A device or emulator that runs API level 26

A Good level of familiarity with Kotlin Programming Language.

An Understanding of Basic Android Fundamentals such as the following is also required.

Kotlin Control Structures such as for Loops and While Loops

If Statements

Canvas

Working with Fragments

Fair knowledge of working with Custom Views

Before we go ahead with building our Signature Capture Application, let’s talk a little about Android Canvas.

Working with Canvas in Kotlin

The Android SDK provides us with a set of APIs for 2D-drawing which allows you to render your custom graphics on canvas or modify the existing Views. You can check out the official Android Developers Guide to learn more about Canvas and Drawables.

Getting Started

To begin we are going to our application by building out some custom classes. Go ahead and create a new android studio project and allow it a complete build. After that create two data directories namely view and utils.

Under the view directory create a Kotlin file/class and name it “ViewTreeObserverCompat”

package com.example.signaturecapture.view import android.annotation.SuppressLint import android.os.Build import android.view.ViewTreeObserver 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: ViewTreeObserver.OnGlobalLayoutListener? ) { // Future (API16+)... if (Build.VERSION.SDK_INT >= 16) { observer.removeOnGlobalLayoutListener(victim) } else { observer.removeGlobalOnLayoutListener(victim) } } } Under the view directory, create another kotlin file named utils and paste the code below: package com.example.signaturecapture.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 } } After that Lets go ahead to the utils Directory and create a class called Bezier and add the following snippets of code to it. package com.example.signaturecapture.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 } }

Please note that if you encounter any errors during this process, simply import the required packages, by clicking the red bulb close to the error or selecting it and using the keyboard shortcut“Alt+ Enter”.



Create another class, named “ControlTimedPoints” and copy and paste the code below. This class simply creates variables and their respective setters and getters for the controlling the points drawn on the screen.

package com.example.signaturecapture.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 } }

Create anther kotlin file class and name it “TimedPoint” and add the following code snippets. This class contains code for measuring velocity and the distance of the drawn object on the canvas in order to provide realtime updates on the screen, whenever a user draws an object.

package com.example.signaturecapture.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() } }

Now lets go to our main java directory and create an interface named “OnSignedCaptureListener”. This interface will simply help to return the captured object drawn by the user on the screen whenever the user finishes drawing and clicks on save.

package com.example.signaturecapture import android.graphics.Bitmap interface OnSignedCaptureListener { fun onSignatureCaptured(bitmap: Bitmap, fileUri: String) }

Create another class that we will employ as a fragment to house three buttons. We will have a button to cancel the process of registering a user’s signature, we will also have button to clear the drawn object on the canvas and another button to save the user’s input on the canvas. Our Fragment will be called “SignatureDialogFragment” and will house the following code.

package com.example.signaturecapture import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import kotlinx.android.synthetic.main.signature_pad.* import com.example.signaturecapture.SignatureView import kotlinx.android.synthetic.main.signature_pad.view.* 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.signature_pad, 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 } }

The SignatureDialogFragment inflates an xml layout, which contains three buttons and makes use of our custom view class, as the pad where the user will enter their signature, as we discussed earlier. So we will go ahead and create a new xml layout, under our layout directory and name it “signature_pad.xml”

<?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="@drawable/rounded_btn_bg" 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="@drawable/rounded_btn_bg" 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="@drawable/rounded_btn_bg" 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.example.signaturecapture.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>

Finally we are going to create our Custom View class which will employ all the other util classes and interface we just created, before going ahead to use it in our main activity. Go ahead and create the custom view class and name it “SignatureView”. Owing to the length of this code snippet, just add this code to your work and take note of the comments added on the essential methods and variables in this class.

package com.example.signaturecapture import android.annotation.TargetApi import android.content.Context import android.content.res.Resources import android.graphics.* import android.os.Build import android.os.Bundle import android.os.Parcelable import android.util.AttributeSet import com.example.signaturecapture.R.styleable import android.view.MotionEvent import android.view.View import android.view.ViewTreeObserver import androidx.annotation.RequiresApi import com.example.signaturecapture.utils.Bezier import com.example.signaturecapture.utils.ControlTimedPoints import com.example.signaturecapture.utils.TimedPoint import com.example.signaturecapture.view.ViewTreeObserverCompat import android.graphics.Bitmap.Config.ARGB_8888 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 } @RequiresApi(Build.VERSION_CODES.KITKAT) 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: Resources.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 } @RequiresApi(Build.VERSION_CODES.KITKAT) fun setSignatureBitmap(signature: Bitmap) { if (isLaidOut()) { 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) setIsEmpty(false) invalidate() } else { viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.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() } }

Now we are finally going to move to our MainActivity, and add an image view and a button, to the layout activity_main.xml file, this will look just like this screen below.

Add the following code to your activity_main.xml file

<?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" tools:context=".MainActivity" > <Button android:id="@+id/buttonShowDialog" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:padding="16dp" android:textColor="@color/colorWhite" android:textSize="18sp" android:background="@drawable/rounded_btn_bg" android:text="@string/enter_signature" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/imageView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/imageView" /> <ImageView android:id="@+id/imageView" android:layout_width="369dp" android:layout_height="470dp" android:padding="16dp" android:scaleType="fitXY" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> </androidx.constraintlayout.widget.ConstraintLayout>

Then we’re finally going to our main activity to add the finishing touches to our Application. Here we are simply going to place an OnClickListener to our button to show the fragment whenever it is clicked on and set the returned image from our SignatureView Custom class to the image view after the user is done with the input activity.

The code sample looks just like this.

package com.example.signaturecapture import android.graphics.Bitmap import androidx.appcompat.app.AppCompatActivity import android.os.Bundle 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) } }

Go ahead and build the Kotlin application and run it to see how it works out. If you encounter any challenges, kindly use the comment section below.

That’s it. Now you have an awesome Signature Capture Application. You can find the source code for this project below.



The source code can be found by clicking HERE



If you found this post on how to build a signature capture application informative or have a question do well to drop a comment below and don’t forget to share with your friends.