前言

又是一年新春,在这里先给大家拜个早年了。每逢春节,写春联贴春联都是一项必不可少的活动。本次主要使用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.curPointval curPoint = ControllerPoint(event.x, event.y)val lineWidth = calWidth(event = event)curPoint.width = lineWidthif (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 / stepsval 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()) {//绘制bitmapdrawImage(bitmap.asImageBitmap(), Offset.Zero)}}
}
  1. 上面提到我们会将笔划绘制到bitmap中,一个字即是一个bitmap
  2. 我们的春联的内容即为上面的bitmap的列表,再加上一个红色的背景
  3. 我们将红色背景与bitmap列表都绘制到一个bitmap中,再将这个bitmap中绘制到Compose
  4. 上面这个bitmap就是我们想要的图片,长按时将其保存到本地即可

相关视频:

【2021最新版】Android studio安装教程+Android(安卓)零基础教程视频(适合Android 0基础,Android初学入门)_哔哩哔哩_bilibili

Android架构设计原理与实战——Jetpack结合MVP组合应用开发一个优秀的APP!_哔哩哔哩_bilibili

Android进阶必学:jetpack架构组件—Navigation_哔哩哔哩_bilibili

Android进阶系统学习——Jetpack先天优秀的基因可以避免数据内存泄漏_哔哩哔哩_bilibili

Compose 实现手写春联效果相关推荐

  1. 用Compose实现手写春联效果

    /   今日科技快讯   / 近日,尽管"元宇宙"这个词已经存在了近30年,但直到Facebook在2021年10月下旬更名为Meta,似乎才重新点燃了投资者对它的兴趣.其中,虚拟 ...

  2. 我做了一个手写春联小网页,祝大家虎年暴富

    目录 前言 产品构思 设计 开发 手写春联:https://cl.xugaoyi.com/ 前言 虎年春节快到了,首先祝大家新年快乐,轻松暴富. 最近在网上经常看到生成春联的文章,不过这些小demo要 ...

  3. 文字描边加粗_用AE制作动态手写文字效果

    现在很多设计师在设计logo或者字体的时候,已经不单纯的停留在固定不动的状态,而是添加了很多动效来提升作品的视觉效果.以现在流行的动效设计来看,可以分为三类:交互动效.MG动效.后期动效. 交互动效: ...

  4. canvas 手写毛笔字效果

    <!doctype html> <html lang="en"> <head><meta charset="UTF-8" ...

  5. html 手写字效果,canvas画布实现手写签名效果的示例代码

    最近项目中涉及到移动端手写签名的功能需求,将实现代码记录于此,供小伙伴们参考指摘哦~ HTML代码: 手写区 清除 确定 CSS样式: .mSign_signMark_box{padding: 15p ...

  6. Adobe Premiere基础-常用的视频特效(边角定位,马赛克,模糊,锐化,手写工具,效果控件层级顺序)(十六)

    边角定位 这个特效非常好使,可以将很多视频都融入到当前视频的场景中,当做广告,操作方式很简单 先在效果里搜索边角定位 我们准备一个素材 然后将边角定位拖动到,自己准备的素材上,然后在效果控件中点击文字 ...

  7. 表白女神专用代码(漂浮的心+手写文字效果)

    <!DOCTYPE html> <html><head><meta charset="UTF-8"><title>练习& ...

  8. Jetpack Compose之手写分享页面

    文章目录 前言 一.xml实现 二.Compose实现 1.底部分享组件 a.背景半圆角 b.自定义分享按钮 c.在底部的取消按钮 d.总体 2.二维码分享组件 a.组件居中 b.顶部头像 c.二维码 ...

  9. android 手写签批_Android手写签名效果

    任何画线的程序,都是先在界面上获取若干不连续的点,然后将这些点连成线. 一些常见的笔型比较好实现,比如说铅笔.钢笔等等,这类笔型的线条的宽度和线条的颜色是固定的,只需要将点连接成固定颜色和固定宽度的线 ...

最新文章

  1. 5-flutter 布局和列表
  2. 开始学习Solaris
  3. 现有工程项目上加响应式
  4. python爬虫简历项目怎么写_python爬虫简历
  5. 比Everything、listary、DocFetcher还好用的桌面文档搜索软件 - bbdoc
  6. charles破解版_Charles抓包工具_charles mac\win7版
  7. js实现椭圆轨迹_Js 椭圆轨迹运动动画 代码分享
  8. 多少 80 后因为一台文曲星而走上了程序员之路
  9. java判断文件是否被占用_java判断一个文件是否正在被其他程序使用(调用)?...
  10. python如何做成app?
  11. 局域网即时通讯软件_企业即时通讯软件需要符合哪些要求?
  12. 磁盘驱动器或Windows Home Server失败的情况挽救了我的婚姻
  13. 读者投稿:阿里 P6 面试体验
  14. php画爱心,在WEB里绘制爱心
  15. 资料外泄:给系统管理者的警告
  16. Anconda 安装
  17. new/delete与malloc/free的区别
  18. 模块一 day05 数据类型(上)
  19. 许多大学仍不愿接受加密货币捐赠
  20. 友盟社会化分享 QQ空间不显示分享的图片 (已解决)

热门文章

  1. 软分叉和硬分叉的区别
  2. linux php com,Linux_COM简介,世上无难事,只要肯登攀, - phpStudy
  3. Visual Studio2017常用快快捷键
  4. 安装双MeeGo系统
  5. 显示桌面图标没有怎么解决
  6. 蘑菇导航源码安装教程,wordpress导航主题免费下载[Wordpress主题]
  7. Selenium_Python实践遇到的问题一:页面存在多窗口时,window_handles属性在保存窗口句柄列表时的顺序不一定和打开页面的顺序一致
  8. Delegate 基本概念
  9. visual assist x_1.2kg华硕灵耀X逍遥体验:4K OLED翻转屏+11代i7处理器
  10. 十年,又回到原点,也许是个新的起点