diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/ControlTimedPoints.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/ControlTimedPoints.kt new file mode 100644 index 0000000000..7fd8d90429 --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/ControlTimedPoints.kt @@ -0,0 +1,11 @@ +package com.navi.amc.investorapp.signature.drawerControllers + +class ControlTimedPoints { + var c1: TimedPoint? = null + var c2: TimedPoint? = null + fun set(c1: TimedPoint?, c2: TimedPoint?): ControlTimedPoints { + this.c1 = c1 + this.c2 = c2 + return this + } +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/CurveBezier.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/CurveBezier.kt new file mode 100644 index 0000000000..b03fd5695f --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/CurveBezier.kt @@ -0,0 +1,52 @@ +package com.navi.amc.investorapp.signature.drawerControllers + +class CurveBezier { + var startPoint: TimedPoint? = null + var control1: TimedPoint? = null + var control2: TimedPoint? = null + var endPoint: TimedPoint? = null + operator fun set( + startPoint: TimedPoint?, control1: TimedPoint?, + control2: TimedPoint?, endPoint: TimedPoint? + ): CurveBezier { + 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 + } + + 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 + } +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/TimedPoint.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/TimedPoint.kt new file mode 100644 index 0000000000..353b0d20b9 --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/drawerControllers/TimedPoint.kt @@ -0,0 +1,31 @@ +package com.navi.amc.investorapp.signature.drawerControllers + +import com.navi.amc.investorapp.util.orZero + +class TimedPoint { + @JvmField + var x = 0f + @JvmField + 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 { + val velocity = distanceTo(start) / (timestamp - start?.timestamp.orZero()) + return if (velocity != velocity) 0f else velocity + } + + fun distanceTo(point: TimedPoint?): Float { + return Math.sqrt( + Math.pow((point?.x.orZero() - x).toDouble(), 2.0) + Math.pow( + (point?.y.orZero() - y).toDouble(), + 2.0 + ) + ).toFloat() + } +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgBuilder.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgBuilder.kt new file mode 100644 index 0000000000..b008284d94 --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgBuilder.kt @@ -0,0 +1,68 @@ +package com.navi.amc.investorapp.signature.svgUtils + +import com.navi.amc.investorapp.signature.drawerControllers.CurveBezier +import java.lang.StringBuilder + +class SvgBuilder { + private val mSvgPathsBuilder = StringBuilder() + private var mCurrentPathBuilder: SvgPathBuilder? = null + fun clear() { + mSvgPathsBuilder.setLength(0) + mCurrentPathBuilder = null + } + + fun build(width: Int, height: Int): String { + if (isPathStarted) { + appendCurrentPath() + } + return StringBuilder() + .append("\n") + .append("") + .append("") + .append(mSvgPathsBuilder) + .append("") + .append("") + .toString() + } + + fun append(curve: CurveBezier, strokeWidth: Float): SvgBuilder { + val roundedStrokeWidth = Math.round(strokeWidth) + val curveStartSvgPoint = SvgPoint(curve.startPoint) + val curveControlSvgPoint1 = SvgPoint(curve.control1) + val curveControlSvgPoint2 = SvgPoint(curve.control2) + val curveEndSvgPoint = SvgPoint(curve.endPoint) + if (!isPathStarted) { + startNewPath(roundedStrokeWidth, curveStartSvgPoint) + } + if (curveStartSvgPoint != mCurrentPathBuilder!!.lastPoint + || roundedStrokeWidth != mCurrentPathBuilder!!.strokeWidth + ) { + appendCurrentPath() + startNewPath(roundedStrokeWidth, curveStartSvgPoint) + } + mCurrentPathBuilder!!.append(curveControlSvgPoint1, curveControlSvgPoint2, curveEndSvgPoint) + return this + } + + private fun startNewPath(roundedStrokeWidth: Int, curveStartSvgPoint: SvgPoint) { + mCurrentPathBuilder = SvgPathBuilder(curveStartSvgPoint, roundedStrokeWidth) + } + + private fun appendCurrentPath() { + mSvgPathsBuilder.append(mCurrentPathBuilder) + } + + private val isPathStarted: Boolean + private get() = mCurrentPathBuilder != null +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgPathBuilder.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgPathBuilder.kt new file mode 100644 index 0000000000..0b98d9336d --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgPathBuilder.kt @@ -0,0 +1,69 @@ +package com.navi.amc.investorapp.signature.svgUtils + +import java.lang.StringBuilder + +class SvgPathBuilder(private val mStartPoint: SvgPoint, val strokeWidth: Int) { + private val mStringBuilder: StringBuilder + var lastPoint: SvgPoint + private set + + fun append( + controlPoint1: SvgPoint, + controlPoint2: SvgPoint, + endPoint: SvgPoint + ): SvgPathBuilder { + mStringBuilder.append(makeRelativeCubicBezierCurve(controlPoint1, controlPoint2, endPoint)) + lastPoint = endPoint + return this + } + + override fun toString(): String { + return StringBuilder() + .append("") + .toString() + } + + private fun makeRelativeCubicBezierCurve( + controlPoint1: SvgPoint, + controlPoint2: SvgPoint, + endPoint: SvgPoint + ): String { + val sControlPoint1 = controlPoint1.toRelativeCoordinates(lastPoint) + val sControlPoint2 = controlPoint2.toRelativeCoordinates(lastPoint) + val sEndPoint = endPoint.toRelativeCoordinates(lastPoint) + val sb = StringBuilder() + sb.append(sControlPoint1) + sb.append(" ") + sb.append(sControlPoint2) + sb.append(" ") + sb.append(sEndPoint) + sb.append(" ") + + // discard zero curve + val svg = sb.toString() + return if ("c0 0 0 0 0 0" == svg) { + "" + } else { + svg + } + } + + companion object { + const val SVG_RELATIVE_CUBIC_BEZIER_CURVE = 'c' + const val SVG_MOVE = 'M' + } + + init { + lastPoint = mStartPoint + mStringBuilder = StringBuilder() + mStringBuilder.append(SVG_RELATIVE_CUBIC_BEZIER_CURVE) + } +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgPoint.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgPoint.kt new file mode 100644 index 0000000000..15b5907ba1 --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/svgUtils/SvgPoint.kt @@ -0,0 +1,52 @@ +package com.navi.amc.investorapp.signature.svgUtils + +import com.navi.amc.investorapp.signature.drawerControllers.TimedPoint +import com.navi.amc.investorapp.signature.svgUtils.SvgPoint +import com.navi.amc.investorapp.util.orZero +import java.lang.StringBuilder + +class SvgPoint { + val x: Int + val y: Int + + constructor(point: TimedPoint?) { + // one optimisation is to get rid of decimals as they are mostly non-significant in the + // produced SVG image + x = Math.round(point?.x.orZero()) + y = Math.round(point?.y.orZero()) + } + + constructor(x: Int, y: Int) { + this.x = x + this.y = y + } + + fun toAbsoluteCoordinates(): String { + val stringBuilder = StringBuilder() + stringBuilder.append(x) + stringBuilder.append(",") + stringBuilder.append(y) + return stringBuilder.toString() + } + + fun toRelativeCoordinates(referencePoint: SvgPoint?): String { + return SvgPoint(x - referencePoint?.x.orZero(), y - referencePoint?.y.orZero()).toString() + } + + override fun toString(): String { + return toAbsoluteCoordinates() + } + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val svgPoint = o as SvgPoint + return if (x != svgPoint.x) false else y == svgPoint.y + } + + override fun hashCode(): Int { + var result = x.hashCode() + result = 31 * result + y.hashCode() + return result + } +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/ui/OnSignedListener.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/ui/OnSignedListener.kt new file mode 100644 index 0000000000..f6dc76cc52 --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/ui/OnSignedListener.kt @@ -0,0 +1,9 @@ +package com.navi.amc.investorapp.signature.ui + +interface OnSignedListener { + fun onStartSigning() + + fun onSigned() + + fun onClear() +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/ui/SilkySignaturePad.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/ui/SilkySignaturePad.kt new file mode 100644 index 0000000000..61e1d6fc67 --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/ui/SilkySignaturePad.kt @@ -0,0 +1,567 @@ +package com.navi.amc.investorapp.signature.ui + +import android.content.Context +import android.content.res.Resources +import android.graphics.* +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.navi.amc.investorapp.R +import com.navi.amc.investorapp.signature.drawerControllers.TimedPoint +import com.navi.amc.investorapp.signature.svgUtils.SvgBuilder +import com.navi.amc.investorapp.signature.drawerControllers.ControlTimedPoints +import com.navi.amc.investorapp.signature.drawerControllers.CurveBezier + +import java.util.ArrayList + +class SilkySignaturePad(context: Context, attrs: AttributeSet?) : View(context, attrs) { + //View state + private var mPoints: MutableList? = null + private var mIsEmpty = false + private var mLastTouchX = 0f + private var mLastTouchY = 0f + private var mLastVelocity = 0f + private var mLastWidth = 0f + private val mDirtyRect: RectF + private val mSvgBuilder = SvgBuilder() + + // Cache + private val mPointsCache: MutableList = ArrayList() + private val mControlTimedPointsCached = ControlTimedPoints() + private val mCurveBezierCached = CurveBezier() + + //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 = 3 + private val DEFAULT_ATTR_PEN_MAX_WIDTH_PX = 7 + 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 + + /** + * 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. + */ + 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 + } + + fun clear() { + mSvgBuilder.clear() + mPoints = ArrayList() + mLastVelocity = 0f + mLastWidth = ((mMinWidth + mMaxWidth) / 2).toFloat() + if (mSignatureBitmap != null) { + mSignatureBitmap = null + ensureSignatureBitmap() + } + isEmpty = true + invalidate() + } + + 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)) + } + MotionEvent.ACTION_MOVE -> { + resetDirtyRect(eventX, eventY) + addPoint(getNewPoint(eventX, eventY)) + } + MotionEvent.ACTION_UP -> { + resetDirtyRect(eventX, eventY) + addPoint(getNewPoint(eventX, eventY)) + parent.requestDisallowInterceptTouchEvent(true) + isEmpty = false + } + 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 + } + + var isEmpty: Boolean + get() = mIsEmpty + private set(newValue) { + mIsEmpty = newValue + if (mOnSignedListener != null) { + if (mIsEmpty) { + mOnSignedListener!!.onClear() + } else { + mOnSignedListener!!.onSigned() + } + } + } + val signatureSvg: String + get() { + val width = transparentSignatureBitmap!!.width + val height = transparentSignatureBitmap!!.height + return mSvgBuilder.build(width, height) + } + val signatureBitmap: Bitmap + get() { + val originalBitmap = transparentSignatureBitmap + val whiteBgBitmap = Bitmap.createBitmap( + originalBitmap!!.width, originalBitmap.height, Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(whiteBgBitmap) + canvas.drawColor(Color.WHITE) + canvas.drawBitmap(originalBitmap, 0f, 0f, null) + return whiteBgBitmap + } + + /** + * @param compressPercentage Hint to the compressor, 0-100 percent. 0 meaning compress for + * small size, 100 meaning compress for max quality. Some + * formats, like PNG which is lossless, will ignore the + * quality setting + */ + fun getCompressedSignatureBitmap(compressPercentage: Int): Bitmap { + var compressPercentage = compressPercentage + if (compressPercentage < 0) { + compressPercentage = 0 + } else if (compressPercentage > 100) { + compressPercentage = 100 + } + val originalBitmap = transparentSignatureBitmap + val originalWidth = originalBitmap!!.width + val originalHeight = originalBitmap.height + val targetWidth = originalWidth * compressPercentage / 100 // your arbitrary fixed limit + val targetHeight = (originalHeight * targetWidth / originalWidth.toDouble()).toInt() + var whiteBgBitmap = + Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(whiteBgBitmap) + canvas.drawColor(Color.WHITE) + canvas.drawBitmap(originalBitmap, 0f, 0f, null) + whiteBgBitmap = Bitmap.createScaledBitmap(originalBitmap, targetWidth, targetHeight, true) + return whiteBgBitmap + } + + /** + * @param deiredWidth Desired width of the bitmap + */ + fun getFixedSizeSignatureBitmap(deiredWidth: Int): Bitmap { + val originalBitmap = transparentSignatureBitmap + val originalWidth = originalBitmap!!.width + val originalHeight = originalBitmap.height + val targetHeight = (originalHeight * deiredWidth // your arbitrary fixed limit + / originalWidth.toDouble()).toInt() + var whiteBgBitmap = + Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(whiteBgBitmap) + canvas.drawColor(Color.WHITE) + canvas.drawBitmap(originalBitmap, 0f, 0f, null) + whiteBgBitmap = Bitmap.createScaledBitmap( + originalBitmap, deiredWidth, targetHeight, true + ) + return whiteBgBitmap + } + + + val transparentSignatureBitmap: Bitmap? + get() { + ensureSignatureBitmap() + return mSignatureBitmap + } + + fun getTransparentSignatureBitmap(trimBlankSpace: Boolean): Bitmap? { + if (!trimBlankSpace) { + return transparentSignatureBitmap + } + 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 val isDoubleClick: Boolean + private get() { + 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) { + this.clear() + return true + } + } + } + return false + } + + private fun getNewPoint(x: Float, y: Float): TimedPoint { + val mCacheSize = mPointsCache.size + val timedPoint: TimedPoint? + timedPoint = if (mCacheSize == 0) { + // Cache is empty, create a new point + TimedPoint() + } else { + // Get point from cache + 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 = calculateCurveControlPoints(mPoints!![0], mPoints!![1], mPoints!![2]) + val c2 = tmp.c2 + recyclePoint(tmp.c1) + tmp = calculateCurveControlPoints(mPoints!![1], mPoints!![2], mPoints!![3]) + val c3 = tmp.c1 + recyclePoint(tmp.c2) + val curve = mCurveBezierCached.set(mPoints!![1], c2, c3, mPoints!![2]) + val startPoint = curve.startPoint + val endPoint = curve.endPoint + var velocity = 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) + + // The Bezier's width starts out as last curve's final width, and + // gradually changes to the stroke width just calculated. The new + // width calculation is based on the velocity between the Bezier's + // start and end mPoints. + 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 = mPoints!![0] + mPoints!!.add(getNewPoint(firstPoint.x, firstPoint.y)) + } + } + + private fun addBezier(curve: CurveBezier, startWidth: Float, endWidth: Float) { + mSvgBuilder.append(curve, (startWidth + endWidth) / 2) + ensureSignatureBitmap() + val originalWidth = mPaint.strokeWidth + val widthDelta = endWidth - startWidth + val drawSteps = Math.floor(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 = 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 = 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 = s1.x - s2.x + val dy1 = s1.y - s2.y + val dx2 = s2.x - s3.x + val dy2 = s2.y - s3.y + val m1X = (s1.x + s2.x) / 2.0f + val m1Y = (s1.y + s2.y) / 2.0f + val m2X = (s2.x + s3.x) / 2.0f + val m2Y = (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 = s2.x - cmX + val ty = s2.y - cmY + return mControlTimedPointsCached.set( + getNewPoint(m1X + tx, m1Y + ty), + getNewPoint(m2X + tx, m2Y + ty) + ) + } + + private fun strokeWidth(velocity: Float): Float { + return Math.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, + Bitmap.Config.ARGB_8888 + ) + mSignatureBitmap?.let { + mSignatureBitmapCanvas = Canvas(it) + } + } + } + + private fun convertDpToPx(dp: Float): Int { + return Math.round(context.resources.displayMetrics.density * dp) + } + + companion object { + private const val DOUBLE_CLICK_DELAY_MS = 200 + } + + init { + val a = context.theme.obtainStyledAttributes( + attrs, + R.styleable.SilkySignaturePad, + 0, 0 + ) + + //Configurable parameters + try { + mMinWidth = a.getDimensionPixelSize( + R.styleable.SilkySignaturePad_penMinWidth, + convertDpToPx(DEFAULT_ATTR_PEN_MIN_WIDTH_PX.toFloat()) + ) + mMaxWidth = a.getDimensionPixelSize( + R.styleable.SilkySignaturePad_penMaxWidth, + convertDpToPx(DEFAULT_ATTR_PEN_MAX_WIDTH_PX.toFloat()) + ) + mPaint.color = a.getColor( + R.styleable.SilkySignaturePad_penColor, + DEFAULT_ATTR_PEN_COLOR + ) + mVelocityFilterWeight = a.getFloat( + R.styleable.SilkySignaturePad_velocityFilterWeight, + DEFAULT_ATTR_VELOCITY_FILTER_WEIGHT + ) + mClearOnDoubleClick = a.getBoolean( + R.styleable.SilkySignaturePad_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() + clear() + } +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/viewHelper/ViewCompat.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/viewHelper/ViewCompat.kt new file mode 100644 index 0000000000..ac329f64a9 --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/viewHelper/ViewCompat.kt @@ -0,0 +1,14 @@ +package com.navi.amc.investorapp.signature.viewHelper + +import android.os.Build +import android.view.View + +object ViewCompat { + fun isLaidOut(view: View): Boolean { + // Future (API19+)... + return if (Build.VERSION.SDK_INT >= 19) { + view.isLaidOut + } else view.width > 0 && view.height > 0 + // Legacy... + } +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/signature/viewHelper/ViewTreeObserverCompat.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/viewHelper/ViewTreeObserverCompat.kt new file mode 100644 index 0000000000..dcc5eaf292 --- /dev/null +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/signature/viewHelper/ViewTreeObserverCompat.kt @@ -0,0 +1,20 @@ +package com.navi.amc.investorapp.signature.viewHelper + +import android.annotation.SuppressLint +import android.os.Build +import android.view.ViewTreeObserver + +object ViewTreeObserverCompat { + @SuppressLint("NewApi") + fun removeOnGlobalLayoutListener( + observer: ViewTreeObserver, + victim: ViewTreeObserver.OnGlobalLayoutListener? + ) { + // Future (API16+)... + if (Build.VERSION.SDK_INT >= 16) { + observer.removeOnGlobalLayoutListener(victim) + } else { + observer.removeGlobalOnLayoutListener(victim) + } + } +} \ No newline at end of file diff --git a/navi-amc/src/main/java/com/navi/amc/investorapp/ui/kyc/fragments/SignatureFragment.kt b/navi-amc/src/main/java/com/navi/amc/investorapp/ui/kyc/fragments/SignatureFragment.kt index 8449679332..9e5428503f 100644 --- a/navi-amc/src/main/java/com/navi/amc/investorapp/ui/kyc/fragments/SignatureFragment.kt +++ b/navi-amc/src/main/java/com/navi/amc/investorapp/ui/kyc/fragments/SignatureFragment.kt @@ -2,15 +2,8 @@ package com.navi.amc.investorapp.ui.kyc.fragments import android.content.Context import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.net.Uri -import android.os.Build import android.os.Bundle -import android.provider.MediaStore import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.PermissionChecker -import androidx.lifecycle.ViewModelProvider import com.navi.amc.investorapp.R import com.navi.amc.investorapp.base.BaseFragment import com.navi.amc.investorapp.constants.AnalyticsConstant @@ -19,22 +12,16 @@ import com.navi.amc.investorapp.constants.Constant.SIGNATURE_UPLOAD import com.navi.amc.investorapp.databinding.FragmentSignatureBinding import com.navi.amc.investorapp.sharedpreference.SharedPreferencesKeys import com.navi.amc.investorapp.sharedpreference.SharedPreferencesWriter +import com.navi.amc.investorapp.signature.ui.OnSignedListener import com.navi.amc.investorapp.ui.kyc.listeners.FragmentInterchangeListener import com.navi.amc.investorapp.ui.kyc.listeners.HeaderInteractionListener -import com.navi.amc.investorapp.ui.kyc.viewmodel.KycVM import com.navi.amc.investorapp.ui.kyc.viewmodel.SignatureVM import com.navi.amc.investorapp.util.IconUtils -import com.navi.amc.investorapp.util.PermissionsManager -import com.navi.amc.investorapp.util.observeNonNull import java.io.ByteArrayOutputStream class SignatureFragment : BaseFragment(), View.OnClickListener { - private val permissionsManager by lazy { PermissionsManager(requireActivity()) } - private val storagePermission by lazy { arrayOf(PermissionsManager.STORAGE_PERMISSION) } - private var isPermissionAlready = false - private var bitmapImage: Bitmap? = null private val analyticsEventTracker = AnalyticsConstant.KycScreen() companion object { @@ -60,7 +47,6 @@ class SignatureFragment : override val layoutRes: Int get() = R.layout.fragment_signature - override fun bindViewModel() { binding.lifecycleOwner = this } @@ -81,46 +67,28 @@ class SignatureFragment : 85, IconUtils.ICON_SIGNATURE ) - binding.uploadIv.visibility = View.VISIBLE - binding.uploadTitleTv.visibility = View.VISIBLE - binding.saveTv.visibility = View.GONE - binding.retakeTv.visibility = View.GONE - binding.permissionDeniedView.visibility = View.GONE - binding.permissionDeniedView.setTitle(getString(R.string.allow_storage_permission)) - binding.permissionDeniedView.setBgColor(R.color.white) - isPermissionAlready = permissionsManager.hasPermissions(storagePermission) - if (!isPermissionAlready) { - permissionsManager.requestPermissions( - storagePermission, - PermissionsManager.REQUEST_CODE - ) - } + disable(binding.saveTv) + disable(binding.redoTv) } private fun initListeners() { binding.saveTv.setOnClickListener(this) - binding.retakeTv.setOnClickListener(this) - binding.uploadTitleTv.setOnClickListener(this) - binding.uploadIv.setOnClickListener(this) - } + binding.redoTv.setOnClickListener(this) + binding.signaturePad.setOnSignedListener(object : OnSignedListener { + override fun onStartSigning() { + enable(binding.saveTv) + enable(binding.redoTv) + } - private fun initAfterScreenCapture(uri: Uri) { - val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap( - ImageDecoder.createSource( - requireContext().contentResolver, - uri - ) - ) - } else { - MediaStore.Images.Media.getBitmap(requireContext().contentResolver, uri) - } - bitmapImage = bitmap - binding.uploadIv.visibility = View.GONE - binding.uploadTitleTv.visibility = View.GONE - binding.saveTv.visibility = View.VISIBLE - binding.retakeTv.visibility = View.VISIBLE - binding.docIv.setImageURI(uri) + override fun onSigned() { + + } + + override fun onClear() { + disable(binding.saveTv) + disable(binding.redoTv) + } + }) } private fun initObservers() { @@ -136,58 +104,22 @@ class SignatureFragment : val accessToken = sharedPreferences?.getObject(SharedPreferencesKeys.COMMON_RESPONSE_OBJECT)?.accessToken.toString() val outputStream = ByteArrayOutputStream() + val bitmapImage = binding.signaturePad.signatureBitmap bitmapImage?.compress(Bitmap.CompressFormat.JPEG, 70, outputStream) viewModel.submitData(accessToken, SIGNATURE_UPLOAD, outputStream.toByteArray()) } - R.id.retake_tv, R.id.upload_iv, R.id.upload_title_tv -> { - if (permissionsManager.hasPermissions(storagePermission) - ) openStorage() else - permissionsManager.requestPermissions( - storagePermission, - PermissionsManager.REQUEST_CODE - ) + R.id.redo_tv -> { + binding.signaturePad.clear() } } } - private val getContent = - registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> - uri?.let { - initAfterScreenCapture(uri) - } - } - - private fun openStorage() { - getContent.launch("image/*") + private fun enable(view: View) { + view.alpha = 1f } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - activity?.let { - val sharedViewModel = ViewModelProvider(it).get(KycVM::class.java) - observePermissionResult(sharedViewModel) - } - } - - private fun observePermissionResult(sharedViewModel: KycVM) { - sharedViewModel.permissionResult.observeNonNull(this) { - when (it?.third) { - PermissionsManager.REQUEST_CODE -> { - if (it.second.firstOrNull() == PermissionChecker.PERMISSION_GRANTED) { - init() - } else { - binding.permissionDeniedView.visibility = View.VISIBLE - } - } - } - sharedViewModel.permissionResult.value = null - } - } - - override fun onResume() { - super.onResume() - if (permissionsManager.hasPermissions(storagePermission) && !isPermissionAlready) { - init() - } + private fun disable(view: View) { + view.alpha = 0.5f } override val screenName: String diff --git a/navi-amc/src/main/res/drawable/progressbar_horizontal_rounded.xml b/navi-amc/src/main/res/drawable/progressbar_horizontal.xml similarity index 100% rename from navi-amc/src/main/res/drawable/progressbar_horizontal_rounded.xml rename to navi-amc/src/main/res/drawable/progressbar_horizontal.xml diff --git a/navi-amc/src/main/res/layout/activity_verify_pan_card.xml b/navi-amc/src/main/res/layout/activity_verify_pan_card.xml index 3c69bca154..a232663ab5 100644 --- a/navi-amc/src/main/res/layout/activity_verify_pan_card.xml +++ b/navi-amc/src/main/res/layout/activity_verify_pan_card.xml @@ -119,7 +119,7 @@ android:layout_height="@dimen/_4dp" android:layout_marginTop="@dimen/_5dp" android:progress="20" - android:progressDrawable="@drawable/progressbar_horizontal_rounded" + android:progressDrawable="@drawable/progressbar_horizontal" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tvProgress" /> diff --git a/navi-amc/src/main/res/layout/fragment_address.xml b/navi-amc/src/main/res/layout/fragment_address.xml index a307a31c71..c59a5a807d 100644 --- a/navi-amc/src/main/res/layout/fragment_address.xml +++ b/navi-amc/src/main/res/layout/fragment_address.xml @@ -76,7 +76,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/_24dp" - android:text="@string/pincode" + android:text="@string/pincode_text" app:layout_constraintStart_toStartOf="@+id/pincode_ev" app:layout_constraintTop_toBottomOf="@+id/address_ev" /> @@ -88,7 +88,7 @@ android:layout_marginStart="@dimen/_8dp" android:layout_marginTop="@dimen/_8dp" android:gravity="center_vertical" - android:hint="@string/pincode" + android:hint="@string/pincode_text" android:imeOptions="actionNext" android:inputType="number" android:maxLength="6" diff --git a/navi-amc/src/main/res/layout/fragment_signature.xml b/navi-amc/src/main/res/layout/fragment_signature.xml index 33f38323b1..6350b72a3c 100644 --- a/navi-amc/src/main/res/layout/fragment_signature.xml +++ b/navi-amc/src/main/res/layout/fragment_signature.xml @@ -2,77 +2,32 @@ - - - - - - - - - - @@ -84,20 +39,11 @@ android:layout_height="@dimen/_48dp" android:layout_marginStart="@dimen/_8dp" android:gravity="center" - android:text="@string/continuee" + android:text="@string/save" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/retake_tv" /> + app:layout_constraintStart_toEndOf="@+id/redo_tv" /> - - \ No newline at end of file diff --git a/navi-amc/src/main/res/layout/view_progress_header.xml b/navi-amc/src/main/res/layout/view_progress_header.xml index 089cab0e00..18bf40b90e 100644 --- a/navi-amc/src/main/res/layout/view_progress_header.xml +++ b/navi-amc/src/main/res/layout/view_progress_header.xml @@ -12,7 +12,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/_20dp" - android:paddingStart="@dimen/_4dp" android:paddingTop="@dimen/_10dp" android:paddingEnd="@dimen/_30dp" android:paddingBottom="@dimen/_10dp" @@ -68,7 +67,7 @@ android:layout_height="@dimen/_4dp" android:layout_marginTop="@dimen/_16dp" android:progress="50" - android:progressDrawable="@drawable/progressbar_horizontal_rounded" + android:progressDrawable="@drawable/progressbar_horizontal" android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/subtitle_tv" /> diff --git a/navi-amc/src/main/res/values-night/attrs.xml b/navi-amc/src/main/res/values-night/attrs.xml new file mode 100644 index 0000000000..c1aab882ec --- /dev/null +++ b/navi-amc/src/main/res/values-night/attrs.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/navi-amc/src/main/res/values/dimens.xml b/navi-amc/src/main/res/values/dimens.xml index 83050a7cea..9a9bda4591 100644 --- a/navi-amc/src/main/res/values/dimens.xml +++ b/navi-amc/src/main/res/values/dimens.xml @@ -37,6 +37,7 @@ 70dp 76dp 80dp + 84dp 88dp 92dp 96dp @@ -91,6 +92,7 @@ 172dp 200dp 205dp + 440dp 55dp 33dp 125dp diff --git a/navi-amc/src/main/res/values/strings.xml b/navi-amc/src/main/res/values/strings.xml index 69758d02d4..586b1e5d9b 100644 --- a/navi-amc/src/main/res/values/strings.xml +++ b/navi-amc/src/main/res/values/strings.xml @@ -134,7 +134,7 @@ Select Bank Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum - Pincode + Pincode Ex - 650087 House no/ Street name House no 10 @@ -542,6 +542,7 @@ Ensure the photo has white background Upload Signature Upload again + Redo Speak the digits below while recording Tips for great video Retake video