之前写的一个带笔画记录功能的安卓画板,最近才有时间写个博客好好介绍一下,参考了一些博客,最后使用了 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)//创建画板bitmapmBitmap = 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.xmStartY = event.y//画笔落笔起点mCurrentPath.moveTo(mStartX, mStartY)}MotionEvent.ACTION_MOVE -> {val previousX = mStartXval previousY = mStartYval dx = abs(event.x - previousX)val dy = abs(event.y - previousY)// 两点之间的距离大于等于3时,生成贝塞尔绘制曲线if (dx >= 3 || dy >= 3) {// 设置贝塞尔曲线的操作点为起点和终点的一半val cX = (event.x + previousX) / 2val cY = (event.y + previousY) / 2// 二阶贝塞尔,实现平滑曲线;previousX, previousY为操作点,cX, cY为终点mCurrentPath.quadTo(previousX, previousY, cX, cY)// 第二次执行时,第一次结束调用的坐标值将作为第二次调用的初始坐标值mStartX = event.xmStartY = event.y}}MotionEvent.ACTION_UP -> {//对当前笔画后的路径出栈var tmp = index + 1while (tmp < pathList.size) {pathList.removeAt(tmp)tmp++}//添加到历史笔画pathList.add(Path(mCurrentPath))index++//将路径画到bitmap中,即一次笔画完成才去更新bitmap,而手势轨迹是实时显示在画板上的。mCanvas.drawPath(mCurrentPath, mPaint)mCurrentPath.reset()}}// 更新绘制invalidate()return true}

这里有三种事件,按下、移动和松开,按下的时候会记录当前路径的起始点,并将当前路径移到起始位置。

移动的时候大致就是将各个点连起来喽,不过这里判断了下距离再做贝塞尔函数连接,特别注意下这里将上一个点作为控制点,而将本次点与上一点的中点作为终点,这个地方是笔画能够流畅的原因,这样做会使笔画具有一定预测方向的能力。

结束的时候会记录本次的路径并绘制出来,添加到记录的数组里面,这里如果是在按下回退之后的路径,还需要先将后退的路径记录清除掉再添加本次路径,最后别忘了重置 mCurrentPath,重置前需要将本次完整的路径画出。

更新路径

    override fun onDraw(canvas: Canvas) {super.onDraw(canvas)//画此次笔画之前的笔画canvas.drawBitmap(mBitmap, 0f, 0f, mPaint)//更新move过程中的笔画mCanvas.drawPath(mCurrentPath, mPaint)}

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

前进后退

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

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

后退
    public override fun back() {if (index < 0) {Toast.makeText(mContext, "当前无旧操作可回退!", Toast.LENGTH_SHORT).show()return}//清空画布mCanvas.drawColor(mBackgroundColor, PorterDuff.Mode.CLEAR)mCanvas.drawColor(mBackgroundColor)//逐步添加路径,并绘制index--var tmp = 0while (tmp <= 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 = mPaintColorpathList.clear()index = -1invalidate()}

这里使用了 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 = 375bottom = 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/813283150 -> 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.absinterface 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 = 10fset(value) {field = valuemPaint.strokeWidth = value}//画笔颜色public var mPaintColor: Int = Color.BLACKset(value) {field = valuemPaint.color = value}//背景色public var mBackgroundColor: Int = Color.TRANSPARENTset(value) {field = valuemCanvas.drawColor(value, PorterDuff.Mode.CLEAR)mCanvas.drawColor(value)var tmp = 0while (tmp <= index) {mCanvas.drawPath(pathList[tmp], mPaint)tmp++}invalidate()}//是否清除边缘空白public var isClearBlank: Boolean = false//手写画笔private val mPaint: Paint = Paint()//起点Xprivate var mStartX = 0f//起点Yprivate var mStartY = 0f//路径private val mCurrentPath: Path = Path()//历史路径private val pathList = LinkedList<Path>()//当前操作位置private var index = -1//画布private lateinit var mCanvas: Canvas//生成的图片private lateinit var mBitmap: Bitmapconstructor(context: Context) : super(context) {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)//创建画板bitmapmBitmap = 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.xmStartY = event.y//画笔落笔起点mCurrentPath.moveTo(mStartX, mStartY)}MotionEvent.ACTION_MOVE -> {val previousX = mStartXval previousY = mStartYval dx = abs(event.x - previousX)val dy = abs(event.y - previousY)// 两点之间的距离大于等于3时,生成贝塞尔绘制曲线if (dx >= 3 || dy >= 3) {// 设置贝塞尔曲线的操作点为起点和终点的一半val cX = (event.x + previousX) / 2val cY = (event.y + previousY) / 2// 二阶贝塞尔,实现平滑曲线;previousX, previousY为操作点,cX, cY为终点mCurrentPath.quadTo(previousX, previousY, cX, cY)// 第二次执行时,第一次结束调用的坐标值将作为第二次调用的初始坐标值mStartX = event.xmStartY = event.y}}MotionEvent.ACTION_UP -> {//对当前笔画后的路径出栈var tmp = index + 1while (tmp < pathList.size) {pathList.removeAt(tmp)tmp++}//添加到历史笔画pathList.add(Path(mCurrentPath))index++//将路径画到bitmap中,即一次笔画完成才去更新bitmap,而手势轨迹是实时显示在画板上的。mCanvas.drawPath(mCurrentPath, mPaint)mCurrentPath.reset()}}// 更新绘制invalidate()return true}public override fun back() {if (index < 0) {Toast.makeText(mContext, "当前无旧操作可回退!", Toast.LENGTH_SHORT).show()return}//清空画布mCanvas.drawColor(mBackgroundColor, PorterDuff.Mode.CLEAR)mCanvas.drawColor(mBackgroundColor)//逐步添加路径,并绘制index--var tmp = 0while (tmp <= 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 = mPaintColorpathList.clear()index = -1invalidate()}/*** 保存画板**/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 = 375bottom = 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/813283150 -> 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

安卓带步骤的手写签名(附源码)相关推荐

  1. 熬夜整理出了70个清华大佬都在用的Python经典练手项目【附源码】

    我们都知道,不管学习那门语言最终都要做出实际的东西来,而对于编程而言,这个实际的东西当然就是项目啦,不用我多说大家都知道学编程语言做项目的重要性. 于是,小编熬了几个通宵,终于整理出了70个清华大佬都 ...

  2. 66个Python练手项目,附源码

    前言: 不管学习哪门语言都希望能做出实际的东西来,这个实际的东西当然就是项目啦,不用多说大家都知道学编程语言一定要做项目才行. 这里整理了66个Python实战项目列表,都有完整且详细的教程,你可以从 ...

  3. 前端进阶-手写Vue2.0源码(三)|技术点评

    前言 今天是个特别的日子 祝各位女神女神节快乐哈 封面我就放一张杀殿的帅照表达我的祝福 哈哈 此篇主要手写 Vue2.0 源码-初始渲染原理 上一篇咱们主要介绍了 Vue 模板编译原理 它是 Vue ...

  4. 【卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10)】

    卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10) 在上一章已经完成了卷积神经网络的结构分析,并通过各个模块理解 ...

  5. 百看不如一练,55个Java练手项目(附源码+视频教程),全都在这里了

    我们都知道,不管学习那门语言最终都要做出实际的东西来,而对于编程而言,这个实际的东西当然就是项目啦,不用我多说大家都知道学编程语言做项目的重要性. 于是,我熬了几个通宵,终于整理出了55个培训机构内部 ...

  6. android米聊手写和涂鸦源码,Android访米聊手写和涂鸦源码

    Android访米聊手写和涂鸦源码 \请下载源代码,只上传Android访米聊手写和涂鸦源码源程序列表内容,如果需要此程序,请点击-下载,下载需要资料源代码. Android访米聊手写和涂鸦源码.ra ...

  7. 一个炫酷的opengles2.0翻页效果(安卓上opengles2.0 翻书效果附源码)

    写了一个opengles2.0的小效果,平台是安卓,给各位朋友们看一看,有兴趣想要源码的朋友可以email我 5358951@qq.com.   先看效果 前言 1.在安卓上实用opengles进行开 ...

  8. 100个Python实战练手项目(附源码+素材),学习必备

    前言: 不管学习哪门语言都希望能做出实际的东西来,这个实际的东西当然就是项目啦,不用多说大家都知道学编程语言一定要做项目才行. 这里整理了最新32个Python实战项目列表,都有完整且详细的视频教程和 ...

  9. android米聊手写和涂鸦源码,涂鸦手写齐上阵 新版米聊将快乐进行到底

    "米聊"是由小米科技出品的一款多平台,跨移动.联通.电信运营商的手机端免费即时通讯工具,通过手机网络(WiFi.3G.GPRS),可以跟你的米聊联系人进行无限量的免费的实时的语音对 ...

最新文章

  1. linux学习中遇到的各种故障与解决方法
  2. 【每日一包0029】merge-descriptors
  3. 171. Leetcode 406. 根据身高重建队列 (贪心算法-两个维度权衡题目)
  4. Objective-C 和 Swift 混编项目的小 Tips(一)
  5. HiccDS共享音乐列表
  6. linux和宿主机windows之间建立共享文件夹
  7. FISCO BCOS Solidity 智能合约 返回json对象、字典mapping、结构体
  8. 使用OpenCore引导黑苹果
  9. 制作一个简易的即时聊天工具
  10. MATLAB热障涂层成像,微波检测热障涂层孔隙率的可行性研究
  11. 电子科大杨宁TCPIP协议原理(总结)
  12. 亚控科技工作中的编程知识小积累
  13. JAVAWEB-NOTE01
  14. SIM7600CE-CNSE 4G模块 树莓派/Windows连网指南
  15. 我读Saliency Filters cvpr 2012
  16. 中断向量,中断向量表 ,中断服务函数
  17. Java 能创建多少线程
  18. 过日子·混日子·奔日子
  19. 图像深度(Image Depth)
  20. handwrite-2

热门文章

  1. iterm2 + oh-my-zsh 让你的命令行用的飞起
  2. Qt --- QTreeWidget 树形控件实例遇到的问题
  3. BigDecimal 往左移动两位小数_人教版小学数学四年级下册 小数点位置移动引起小数大小的变化 教案、课件,公开课视频...
  4. Qt发布exe软件及修改exe应用程序图标
  5. 神奇的操作,用买家手机号查询顺丰物流信息
  6. python研究股价_用python处理月度股价数据
  7. ACCESS+ADO学习记录一点点
  8. Vue.js入门学习--列表渲染--v-for遍历数组生成元素(四)
  9. NOTES常见问题及解决方法
  10. anylogic 学习(3)—— 智能体相关操作