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 below resource file.
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>
styles.xml
<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.
SignatureView.java.
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