安卓带步骤的手写签名(附源码)

您所在的位置:网站首页 安卓手机手写功能 安卓带步骤的手写签名(附源码)

安卓带步骤的手写签名(附源码)

2024-02-18 04:38| 来源: 网络整理| 查看: 265

之前写的一个带笔画记录功能的安卓画板,最近才有时间写个博客好好介绍一下,参考了一些博客,最后使用了 kotlin 实现的,虽然用起来很爽,可是过了一段时间再看自己都有点懵,还好当时留下的注释非常多,有助于理解,下面是 github 源码,欢迎 star 和收藏!

https://github.com/silencefly96/drawdemo

效果图

在这里插入图片描述

实现思路

这里是一个带笔画记录功能的画板,我思考了一下大概需要有前进、后退、清除及导出功能,还是先写了一个接口,感觉有助于编写功能:

interface IDrawableView { fun back() fun forward() fun clear() fun bitmap() : Bitmap @Throws(IOException::class) fun output(path: String) }

其中 bitmap 方法是获得自定义视图的 bitmap,output 会向指定文件名导出 png 图片,都算导出吧。

初始化 private fun init(context: Context) { mContext = context //设置抗锯齿 mPaint.isAntiAlias = true //设置签名笔画样式 mPaint.style = Paint.Style.STROKE //设置笔画宽度 mPaint.strokeWidth = mPaintWidth //设置签名颜色 mPaint.color = mPaintColor } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) //创建画板bitmap mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) //画板 mCanvas = Canvas(mBitmap) //背景 mCanvas.drawColor(mBackgroundColor) }

这里 init 函数在构造函数里设置画笔信息,onSizeChanged 方法里会创建默认颜色的画布。

手指事件 override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { mStartX = event.x mStartY = event.y //画笔落笔起点 mCurrentPath.moveTo(mStartX, mStartY) } MotionEvent.ACTION_MOVE -> { val previousX = mStartX val previousY = mStartY val dx = abs(event.x - previousX) val dy = abs(event.y - previousY) // 两点之间的距离大于等于3时,生成贝塞尔绘制曲线 if (dx >= 3 || dy >= 3) { // 设置贝塞尔曲线的操作点为起点和终点的一半 val cX = (event.x + previousX) / 2 val cY = (event.y + previousY) / 2 // 二阶贝塞尔,实现平滑曲线;previousX, previousY为操作点,cX, cY为终点 mCurrentPath.quadTo(previousX, previousY, cX, cY) // 第二次执行时,第一次结束调用的坐标值将作为第二次调用的初始坐标值 mStartX = event.x mStartY = event.y } } MotionEvent.ACTION_UP -> { //对当前笔画后的路径出栈 var tmp = index + 1 while (tmp super.onDraw(canvas) //画此次笔画之前的笔画 canvas.drawBitmap(mBitmap, 0f, 0f, mPaint) //更新move过程中的笔画 mCanvas.drawPath(mCurrentPath, mPaint) }

更新的时候实际是在上一次的 bitmap 的基础上,绘制本次路径,两者叠加就是全部图形。

前进后退 //路径 private val mCurrentPath: Path = Path() //历史路径 private val pathList = LinkedList() //当前操作位置 private var index = -1

先熟悉下我们前进后退需要用到的几个全局变量,然后先将后退,再说前进。

后退 public override fun back() { if (index mCanvas.drawPath(pathList[tmp], mPaint) tmp++ } invalidate() }

这里就是根据当前位置的 index,清空画布后,再重绘到 index 前一个路径记录,同时 index 减一。这里性能可能很差劲,但是能用,如果读者有什么好办法可以在评论中指出!

前进 public override fun forward() { if (index >= pathList.size - 1) { Toast.makeText(mContext, "当前无旧操作可前进!", Toast.LENGTH_SHORT).show() return } //只需要画下一笔 mCanvas.drawPath(pathList[++index], mPaint) invalidate() }

前进比起后退更简单了,如果有下一笔,画出来就可以了。

清除画布 public override fun clear() { //更新画板信息 mCanvas.drawColor(mBackgroundColor, PorterDuff.Mode.CLEAR) mCanvas.drawColor(mBackgroundColor) mPaint.color = mPaintColor pathList.clear() index = -1 invalidate() }

这里使用了 PorterDuff.Mode.CLEAR 来清除后,还需要使用默认颜色再绘制一遍,很鸡肋,这里还要重置一下各个变量。

导出图片 @Throws(IOException::class) override fun output(path: String) { //配置是否去除边缘 val bitmap = when(isClearBlank) { true -> clearBlank(mBitmap) false -> mBitmap } val bos = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos) val buffer: ByteArray = bos.toByteArray() val file = File(path) if (file.exists()) { file.delete() } val outputStream: OutputStream = FileOutputStream(file) outputStream.write(buffer) outputStream.close() }

这里就一个导出功能,用到了 bitmap 压缩成 PNG 的方法,不是很难。这里还有一个去除白边的功能,是看得别人的,想想可能用到,还是留了下来,优化了一下,可能不太好理解。

private fun clearBlank(bitmap: Bitmap): Bitmap { //扫描各边距不等于背景颜色的第一个点 val top = getDifferentFromArray(0, bitmap.width, bitmap, 0 until bitmap.height) var bottom = getDifferentFromArray(0, bitmap.width, bitmap, bitmap.height - 1 downTo 0) val left = getDifferentFromArray(1, bitmap.height, bitmap, 0 until bitmap.width) var right = getDifferentFromArray(1, bitmap.height, bitmap, bitmap.width - 1 downTo 0) //防止创建null的bitmap 引发的崩溃 if (left == 0 && top == 0 && right == 0 && bottom == 0) { right = 375 bottom = 375 } return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top) }

主要就是获得四个方向第一次有数据的点的位置,在创建 bitmap,这样出来的图像就等于完美压缩了一般。下面这个方法是对 bitmap 的处理,这里为了能够把四个地方共用,传了一个 array 参数描述处理的方向:

private fun getDifferentFromArray(type: Int, length: Int, bitmap: Bitmap, array: IntProgression): Int { val pixels = IntArray(length) for (i in array) { when(type) { //https://blog.csdn.net/tanmx219/article/details/81328315 0 -> bitmap.getPixels(pixels, 0, length, 0, i, length, 1) //获得一行 1 -> bitmap.getPixels(pixels, 0, 1, i, 0, 1, length) //获得一列 else -> {} } for (j in pixels) { if (j != mBackgroundColor) { return i } } } return 0 }

关于 bitmap 处理的一些知识可以看这篇博客,很有帮助

https://blog.csdn.net/tanmx219/article/details/81328315

完整代码

虽然给出了 GitHub 链接还是贴一下完整代码吧,毕竟 GitHub 也就拿这个用了一下。

import android.annotation.SuppressLint import android.content.Context import android.graphics.* import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.widget.Toast import java.io.* import java.util.* import kotlin.math.abs interface IDrawableView { fun back() fun forward() fun clear() fun bitmap() : Bitmap @Throws(IOException::class) fun output(path: String) } @Suppress("RedundantVisibilityModifier") class DrawableView : View, IDrawableView { private lateinit var mContext: Context //画笔宽度 px; public var mPaintWidth = 10f set(value) { field = value mPaint.strokeWidth = value } //画笔颜色 public var mPaintColor: Int = Color.BLACK set(value) { field = value mPaint.color = value } //背景色 public var mBackgroundColor: Int = Color.TRANSPARENT set(value) { field = value mCanvas.drawColor(value, PorterDuff.Mode.CLEAR) mCanvas.drawColor(value) var tmp = 0 while (tmp init(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { init(context) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { init(context) } private fun init(context: Context) { mContext = context //设置抗锯齿 mPaint.isAntiAlias = true //设置签名笔画样式 mPaint.style = Paint.Style.STROKE //设置笔画宽度 mPaint.strokeWidth = mPaintWidth //设置签名颜色 mPaint.color = mPaintColor } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) //创建画板bitmap mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) //画板 mCanvas = Canvas(mBitmap) //背景 mCanvas.drawColor(mBackgroundColor) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) //画此次笔画之前的笔画 canvas.drawBitmap(mBitmap, 0f, 0f, mPaint) //更新move过程中的笔画 mCanvas.drawPath(mCurrentPath, mPaint) } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { mStartX = event.x mStartY = event.y //画笔落笔起点 mCurrentPath.moveTo(mStartX, mStartY) } MotionEvent.ACTION_MOVE -> { val previousX = mStartX val previousY = mStartY val dx = abs(event.x - previousX) val dy = abs(event.y - previousY) // 两点之间的距离大于等于3时,生成贝塞尔绘制曲线 if (dx >= 3 || dy >= 3) { // 设置贝塞尔曲线的操作点为起点和终点的一半 val cX = (event.x + previousX) / 2 val cY = (event.y + previousY) / 2 // 二阶贝塞尔,实现平滑曲线;previousX, previousY为操作点,cX, cY为终点 mCurrentPath.quadTo(previousX, previousY, cX, cY) // 第二次执行时,第一次结束调用的坐标值将作为第二次调用的初始坐标值 mStartX = event.x mStartY = event.y } } MotionEvent.ACTION_UP -> { //对当前笔画后的路径出栈 var tmp = index + 1 while (tmp if (index mCanvas.drawPath(pathList[tmp], mPaint) tmp++ } invalidate() } public override fun forward() { if (index >= pathList.size - 1) { Toast.makeText(mContext, "当前无旧操作可回退!", Toast.LENGTH_SHORT).show() return } //只需要画下一笔 mCanvas.drawPath(pathList[++index], mPaint) invalidate() } /** * 清除画板 */ public override fun clear() { //更新画板信息 mCanvas.drawColor(mBackgroundColor, PorterDuff.Mode.CLEAR) mCanvas.drawColor(mBackgroundColor) mPaint.color = mPaintColor pathList.clear() index = -1 invalidate() } /** * 保存画板 * */ public override fun bitmap(): Bitmap { return mBitmap } /** * 保存画板 * @param path 保存到路径 * */ @Throws(IOException::class) override fun output(path: String) { //配置是否去除边缘 val bitmap = when(isClearBlank) { true -> clearBlank(mBitmap) false -> mBitmap } val bos = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos) val buffer: ByteArray = bos.toByteArray() val file = File(path) if (file.exists()) { file.delete() } val outputStream: OutputStream = FileOutputStream(file) outputStream.write(buffer) outputStream.close() } /** * 逐行扫描 清楚边界空白。 * * @param bitmap * @return */ private fun clearBlank(bitmap: Bitmap): Bitmap { //扫描各边距不等于背景颜色的第一个点 val top = getDifferentFromArray(0, bitmap.width, bitmap, 0 until bitmap.height) var bottom = getDifferentFromArray(0, bitmap.width, bitmap, bitmap.height - 1 downTo 0) val left = getDifferentFromArray(1, bitmap.height, bitmap, 0 until bitmap.width) var right = getDifferentFromArray(1, bitmap.height, bitmap, bitmap.width - 1 downTo 0) //防止创建null的bitmap 引发的崩溃 if (left == 0 && top == 0 && right == 0 && bottom == 0) { right = 375 bottom = 375 } return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top) } private fun getDifferentFromArray(type: Int, length: Int, bitmap: Bitmap, array: IntProgression): Int { val pixels = IntArray(length) for (i in array) { when(type) { //https://blog.csdn.net/tanmx219/article/details/81328315 0 -> bitmap.getPixels(pixels, 0, length, 0, i, length, 1) //获得一行 1 -> bitmap.getPixels(pixels, 0, 1, i, 0, 1, length) //获得一列 else -> {} } for (j in pixels) { if (j != mBackgroundColor) { return i } } } return 0 } } 结语

其实还有设置笔画粗细、颜色之类的没说,具体看源码里面的使用,好了,性能虽然不怎么样,可是这带记录笔画功能的安卓画板可是重来没在各个博客上看到过哦!!!

end



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3