Giter VIP home page Giter VIP logo

compose-handwriting's Introduction

前言

又是一年新春,在这里先给大家拜个早年了。每逢春节,写春联贴春联都是一项必不可少的活动。本次主要使用Compose,实现手写春联的效果。如果对你有所帮助,欢迎点个赞或者评论鼓励一下~

爆竹声中一岁除
春风送暖入屠苏
千门万户曈曈日
总把新桃换旧符

效果图

生成的春联

主要思路

事件监听

我们需要实现手写春联效果,首先就是要做事件监听,Android中自然是监听Action_DownAction_MoveAction_UPCompose中应该如何处理呢?
其实Compose中也可以利用pointerInteropFilter监听Action_DownAction_MoveAction_UP,如下所示

Column(modifier = Modifier.pointerInteropFilter {
    when (it.action) {
        MotionEvent.ACTION_DOWN -> {}
        MotionEvent.ACTION_MOVE -> {}
        MotionEvent.ACTION_UP -> {}
        else ->  false
    }
     true
})

路径绘制

当我们手写春联的时候,实际上就是把我们触摸过的点连接起来,最直接的想法当然是通过Path来绘制,即把各个点连接成Path,然后通过drawPath来绘制
但是问题在于春联是毛笔效果,在写的过程中路径的粗细会发生变化,而drawPath只支持固定的宽度,因此不符合我们的要求。

所以我们换个思路,drawPath其实也是将各个点连接起来,如果我们将触摸过程中的点记录下来,然后在这一系列的点上画圆不就行了吗?每个圆的半径可以自定义,但这样会带来以下问题

可以看出:android触摸中的MOVE时间取点的频率不是非常高,会隔一定的像素取点。当轻触滑动时会出现不连续圆的情况,明显不符合笔锋效果

贝塞尔曲线

上面的问题在于MOVE过程中回调的次数有限,因此只会产生一系列不连续的点,而不是一条线,该如何解决呢?
我们可以想一下Path,其实它也只是定义了一系列的点,然后通过贝塞尔曲线将这些点连接起来,从而实现了曲线效果,我们是不是也可以通过类似的方式,将上面这些点连成线呢?

    private fun onActionMove(event: MotionEvent) {
        val lastPoint = viewStates.value.curPoint
        val curPoint = ControllerPoint(event.x, event.y)
        val lineWidth = calWidth(event = event)
        curPoint.width = lineWidth
        if (viewStates.value.pointList.size < 2) {
            //初始化贝塞尔曲线
            bezier.init(lastPoint, curPoint)
        } else {
            //添加下一个点
            bezier.addNode(curPoint)
        }
        val curDis = getDistance(event)
        //在两个点之间插入10个点,它们都在两个点连接的贝塞尔曲线上
        val steps: Int = 1 + (curDis / STEP_FACTOR).toInt()
        val step = 1.0 / steps
        val list = mutableListOf<ControllerPoint>()
        var t = 0.0
        // 插入10个点
        while (t < 1.0) {
            val point: ControllerPoint = bezier.getPoint(t)
            list.add(point)
            t += step
        }
        addPoints(list)
        _viewStates.value = _viewStates.value.copy(curPoint = curPoint)
    }

如上所示,主要做了以下工作:

  1. 当目前列表中只有1个点时,初始化贝塞尔曲线,即以上一个点为起始点,当前点为终点
  2. 当列表中已经有2个点时,往贝塞尔曲线中加入当前点,将原来的终点变为起点,当前点变为新的终点
  3. 在贝塞尔曲线的起点与终点之间插入多个点,它们的位置都在贝塞尔曲线上,具体的数量由STEP_FACTOR决定,我们目前暂定为10个
  4. 当我们在2个点之间,插入了多个点之后,它们之间的空白就会被填补,看起来不像一条线一样

可变的宽度

上文说到毛笔的路径粗细是不断变化的,一般来说是越慢的地方笔划越粗,越快的地方笔划越细,同时两个相邻的点之间的宽度应该是渐变的,而不是突变的,计算笔画宽度的代码如下:

    private fun calWidth(event: MotionEvent): Float {
    	// 滑动距离
        val distance = getDistance(event)
        // 滑动距离加个影响系数定义为速度
        val calVel = distance * 0.002
        // 速度越大宽度越小,速度越小宽度越大
        val width = NORMAL_WIDTH * maxOf(exp(-calVel), 0.2)
        return width.toFloat()
    }

如上所示,主要做了以下工作:

  1. 虽然决定笔划粗细的是速度,但我们可以假定两次MOVE的间隔是大致相同的,因此计算出滑动距离即可
  2. 因为我们希望笔划粗细有个最大值与最小值,因此我们需要给滑动距离加个影响系数,使exp(-calVel)的结果尽量在0.2与1之间
  3. 速度越大宽度越小,速度越小宽度越大,当滑动速度为0时,exp(-calVel)即为1,而滑动速度越快,exp(-calVel)越接近于0

上面主要是MOVE回调的点的宽度计算,除了MOVE回调的点,贝塞尔曲线加入的点的宽度也应该在起点与终点的宽度之间渐变

    private double getW(double t){
        return getWidth(mSource.width, mDestination.width, t);
    }

    private double getWidth(double w0, double w1, double t){
        return w0 + (w1 - w0) * t;
    }

绘制性能优化

上面我们通过保存MOVE过程中的点的方式实现绘制,当随着笔划越来越多,需要绘制的点也越来越多,在onDraw中对列表进行遍历然后绘制是比较耗性能的,同时每当列表更新,列表都会重新遍历
我们可以建立一个缓冲bitmapACTION_UP事件中将当前所有点绘制到缓冲bitmap中。在draw时直接将缓冲bitmap绘制到canvas中,如下所示:

fun SpringBoard() {
	//定义内存图片
	val bitmap = remember {
        Bitmap.createBitmap(itemSize.toInt(), itemSize.toInt(), Bitmap.Config.ARGB_8888)
    }
    val newCanvas = remember { android.graphics.Canvas(bitmap) }
    val paint = remember { Paint().apply { color = android.graphics.Color.BLACK } }
    BoxWithConstraints(){
        Canvas(
            modifier = Modifier
                .fillMaxSize()
                .pointerInteropFilter(onTouchEvent = {
                    when (it.action) {
                        MotionEvent.ACTION_UP -> {
                            //绘制到Bitmap上
                            states.pointList.forEach { point ->
                                newCanvas.drawCircle(point.x, point.y, point.width, paint)
                            }
                            //清空当前列表
                            viewModel.dispatch(SpringBoardViewAction.ActionUp(it))
                        }
                    }
                    true
                })
        ) {
            //绘制Bitmap,即之前的笔划
            drawImage(bitmap.asImageBitmap())
            //绘制当前列表,即当前笔划
            states.pointList.forEach {
                drawCircle(Color.Black, it.width, Offset(it.x, it.y))
            }
        }
    }
}

如上所示,主要做了以下工作:

  1. 定义内存图片bitmap,并通过bitmap获取newCanvas
  2. ACTION_UP时将当前点的列表绘制到bitmap中并且清空当前点的列表
  3. onDraw中绘制bitmap,即绘制之前的笔划,同时绘制当前的pointList,即当前的笔划

长按保存到本地

我们在长按时,需要将春联保存到本地,这需要我们把Compose代码转化成Bitmap,这个在View中比较成熟,但是在Compose中我没有找到相关方法
我们可以仿照上面的实现,把内容绘制在一个bitmap上,然后直接保存这个bitmap上就好了

@Composable
fun SpringPreview() {
    BoxWithConstraints(
        modifier = Modifier
            .pointerInput(Unit) {
                detectTapGestures(
                    onLongPress = {
                    	//长按时保存bitmap到本地
                        BitmapUtils.saveBitmapToGallery(context, bitmap, "春联")
                    }
                )
            }
    ) {
    	//将背景与图片列表绘制到bitmap上
        newCanvas.drawColor(android.graphics.Color.RED)
        for (i in states.bitmapList.indices) {
            newCanvas.drawBitmap(states.bitmapList[i], 0f, itemSize * i, paint)
        }

        Canvas(modifier = Modifier.fillMaxSize()) {
            //绘制bitmap
            drawImage(bitmap.asImageBitmap(), Offset.Zero)
        }
    }
}
  1. 上面提到我们会将笔划绘制到bitmap中,一个字即是一个bitmap
  2. 我们的春联的内容即为上面的bitmap的列表,再加上一个红色的背景
  3. 我们将红色背景与bitmap列表都绘制到一个bitmap中,再将这个bitmap中绘制到Compose
  4. 上面这个bitmap就是我们想要的图片,长按时将其保存到本地即可

总结

参考资料

安卓画笔笔锋的实现探索(一)
过年了!开发一套纸笔系统,随时随地在线写春联

项目地址

https://github.com/shenzhen2017/compose-handwriting

compose-handwriting's People

Contributors

ricardojiang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.