之前写的一个带笔画记载功用的安卓画板,最近才有时刻写个博客好好介绍一下,参阅了一些博客,最终使用了 kotlin 完成的,尽管用起来很爽,可是过了一段时刻再看自己都有点懵,还好其时留下的注释十分多,有助于了解,下面是 github 源码,欢迎 star 和收藏!

github.com/silencefly9…

效果图

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

完成思路

这儿是一个带笔画记载功用的画板,我考虑了一下大概需求有行进、撤退、铲除及导出功用,仍是先写了一个接口,感觉有助于编写功用:

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)
                    // 第2次执行时,第一次完毕调用的坐标值将作为第2次调用的初始坐标值
                    mStartX = event.x
                    mStartY = event.y
                }
            }
            MotionEvent.ACTION_UP -> {
                //对当时笔画后的途径出栈
                var tmp = index + 1
                while (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 = 0
        while (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 = 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 处理的一些知识能够看这篇博客,很有协助

blog.csdn.net/tanmx219/ar…

完好代码

尽管给出了 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 <= index) {
                mCanvas.drawPath(pathList[tmp], mPaint)
                tmp++
            }
            invalidate()
        }
    //是否铲除边际空白
    public var isClearBlank: Boolean = false
    //手写画笔
    private val mPaint: Paint = Paint()
    //起点X
    private var mStartX = 0f
    //起点Y
    private 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: Bitmap
    constructor(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)
        //创立画板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)
                    // 第2次执行时,第一次完毕调用的坐标值将作为第2次调用的初始坐标值
                    mStartX = event.x
                    mStartY = event.y
                }
            }
            MotionEvent.ACTION_UP -> {
                //对当时笔画后的途径出栈
                var tmp = index + 1
                while (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 = 0
        while (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 = 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