Build a Signature Capture Application Using Canvas & Kotlin
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.