Android实现点汇聚成字的动态效果详解
作者:岛上码农 发布时间:2023-10-08 08:50:26
前言
在引入 fl_chart 绘制图表的时候,看到插件有下面这样的动效,随机散乱的圆点最后组合成了 Flutter 的 Logo,挺酷炫的。本篇我们来探讨类似的效果怎么实现。
点阵
在讲解代码实现之前,我们先科普一个知识,即点阵。点阵在日常生活中很常见,比如广告屏,停车系统的显示,行业内称之为 LED 显示屏。
LED 显示屏实际上就是由很多 LED 灯组合成的一个显示面板,然后通过显示驱动某些灯亮,某些灯灭就可以实现文字、图形的显示。LED 显示屏的点距足够小时,色彩足够丰富时其实就形成了我们日常的显示屏,比如 OLED 显示屏其实原理也是类似的。之前报道过的大学宿舍楼通过控制每个房间的灯亮灯灭来形成文字的原理也是一样的。
现在来看看 LED显示文字是怎么回事,比如我们要 显示岛上码农的“岛”字,在16x16的点阵上,通过排布得到的就是下面的结果(不同字体的排布会有些差别)。
因为每一行是16个点,我们可以对应为16位二进制数,把黑色的标记为1,灰色的标记为0,每一行就可以得到一个二进制数。比如上面的第一行第8列为1,其他都是0,对应的二进制数就是0000000100000000,对应的16进制数就是0x0100。把其他行也按这种方式计算出来,最终得到的“岛”字对应的是16个16进制数,如下所示。
[
0x0100, 0x0200, 0x1FF0, 0x1010,
0x1210, 0x1150, 0x1020, 0x1000,
0x1FFC, 0x0204, 0x2224, 0x2224,
0x3FE4, 0x0004, 0x0028, 0x0010
];
又了这个基础,我们就可以用 Flutter 绘制点阵图形。
点阵图形绘制
首先我们绘制一个“LED 面板”,也就是绘制一个有若干个点构成的矩阵,这个比较简单,保持相同的间距,逐行绘制相同的圆即可,比如我们绘制一个16x16的点阵,实现代码如下所示。
var paint = Paint()..color = Colors.grey;
final dotCount = 16;
final fontSize = 100.0;
var radius = fontSize / dotCount;
var startPos =
Offset(size.width / 2 - fontSize, size.height / 2 - 2 * fontSize);
for (int i = 0; i < dotCount; ++i) {
var position = startPos + Offset(0.0, radius * i * 2);
for (int j = 0; j < dotCount; ++j) {
var dotPosition = startPos + Offset(radius * 2 * j, position.dy);
canvas.drawCircle(dotPosition, radius, paint);
}
}
绘制出来的效果如下:
接下来是点亮对应的位置来绘制文字了。上面我们讲过了,每一行是一个16进制数,那么我们只需要判断每一行的16进制数的第几个 bit是1就可以了,如果是1就点亮,否则不点亮。点亮的效果用不同的颜色就可以了。 怎么判断16进制数的第几个 bit 是不是1呢,这个就要用到位运算技巧了。实际上,我们可以用一个第 N 个 bit 是1,其他 bit 都是0的数与要判断的数进行“位与”运算,如果结果不为0,说明要判断的数的第 N 个 bit 是1,否则就是0。听着有点绕,看个例子,我们以0x0100为例,按从第0位到第15位逐个判断第0位和第15位是不是1,代码如下:
for (i = 0 ; i < 16; ++i) {
if ((0x0100 & (1 << i)) > 0) {
// 第 i 位为1
}
}
这里有两个位操作,1 << i
是将1左移 i 位,为什么是这样呢,因为这样可以构成0x0001,0x0002,0x0004,...,0x8000等数字,这些数字依次从第0位,第1位,第2位,...,第15位为1,其他位都是0。然后我们用这样的数与另外一个数做位与运算时,就可以依次判断这个数的第0位,第1位,第2位,...,第15位是否为1了,下面是一个计算示例,第11位为1,其他位都是0,从而可以 判断另一个数的第11位是不是0。
通过这样的逻辑我们就可以判断一行的 LED 中第几列应该点亮,然后实现文字的“显示”了,实现代码如下。wordHex
是对应字的16个16进制数的数组。dotCount
的值是16,用于控制绘制16x16大小的点阵。每隔一行我们向下移动一段直径距离,每隔一列,我们向右移动一段直径距离。然后如果当前绘制位置的数值对应的 bit位为1,就用蓝色绘制,否则就用灰色绘制。这里说一下为什么左移的时候要用dotCount - j - 1
,这是因为绘制是从左到右的,而16进制数的左边是高位,而数字j是从小到大递增的,因此要通过这种方式保证判断的顺序是从高位(第15位)到低位(第0位),和绘制的顺序保持一致。
for (int i = 0; i < dotCount; ++i) {
var position = startPos + Offset(0.0, radius * i * 2);
for (int j = 0; j < dotCount; ++j) {
var dotPosition = startPos + Offset(radius * 2 * j, position.dy);
if ((wordHex[i] & ((1 << dotCount - j - 1))) != 0) {
paint.color = Colors.blue[600]!;
canvas.drawCircle(dotPosition, radius, paint);
} else {
paint.color = Colors.grey;
canvas.drawCircle(dotPosition, radius, paint);
}
}
}
绘制的结果如下所示。
由点聚集成字的动画实现
接下来我们来考虑如何实现开篇说的类似的动画效果。实际上方法也很简单,就是先按照文字应该“点亮”的 LED 的数量,先在随机的位置绘制这么多数量的 LED,然后通过动画控制这些 LED 移动到目标位置——也就是文字本该绘制的位置。这个移动的计算公式如下,其中 t 是动画值,取值范围为0-1.
需要注意的是,随机点不能在绘图过程生成,那样会导致每次绘制产生新的随机位置,也就是初始位置会变化,导致上面的公式实际不成立,就达不到预期的效果。另外,也不能在 build
方法中生成,因为每次刷新 build 方法就会被调用,同样会导致初始位置发生变化。所以,生成随机位置应该在 initState
方法完成。但是又遇到一个新问题,那就是 initState
方法里没有 context
,拿不到屏幕宽高,所以不能直接生成位置,我们只需要生成一个0-1
的随机系数就可以了,然后在绘制的时候在乘以屏幕宽高就得到实际的初始位置了。初始位置系数生成代码如下:
@override
void initState() {
super.initState();
var wordBitCount = 0;
for (var hex in dao) {
wordBitCount += _countBitOne(hex);
}
startPositions = List.generate(wordBitCount, (index) {
return Offset(
Random().nextDouble(),
Random().nextDouble(),
);
});
...
}
wordBitCount
是计算一个字中有多少 bit 是1的,以便知道要绘制的 “LED” 数量。接下来是绘制代码了,我们这次对于不亮的直接不绘制,然后要点亮的位置通过上面的位置计算公式计算,这样保证了一开始绘制的是随机位置,随着动画的过程,逐步移动到目标位置,最终汇聚成一个字,就实现了预期的动画效果,代码如下。
void paint(Canvas canvas, Size size) {
final dotCount = 16;
final fontSize = 100.0;
var radius = fontSize / dotCount;
var startPos =
Offset(size.width / 2 - fontSize, size.height / 2 - fontSize);
var paint = Paint()..color = Colors.blue[600]!;
var paintIndex = 0;
for (int i = 0; i < dotCount; ++i) {
var position = startPos + Offset(0.0, radius * i * 2);
for (int j = 0; j < dotCount; ++j) {
// 判断第 i 行第几位不为0,不为0则绘制,否则不绘制
if ((wordHex[i] & ((1 << dotCount - j))) != 0) {
var startX = startPositions[paintIndex].dx * size.width;
var startY = startPositions[paintIndex].dy * size.height;
var endX = startPos.dx + radius * j * 2;
var endY = position.dy;
var animationPos = Offset(startX + (endX - startX) * animationValue,
startY + (endY - startY) * animationValue);
canvas.drawCircle(animationPos, radius, paint);
paintIndex++;
}
}
}
}
来看看实现效果吧,是不是很酷炫?完整源码已提交至:绘图相关源码,文件名为:dot_font.dart
。
来源:https://juejin.cn/post/7120233450627891237


猜你喜欢
- Java中四种访问权限总结一、Java中有四种访问权限, 其中三种有访问权限修饰符,分别为private、public、prot
- collection标签的oftype属性能否为java.util.Map基于mybatis-3.4.5.jar版本,结论是可以的。<
- QTableWidget 是 Qt 中的表格组件类。在窗体上放置一个 QTableWidget 组件后,可以在 Property Edito
- 概述最近重新回顾了一下数据结构和算法的一些基本知识,对几种排序算法有了更多的理解,也趁此机会通过博客做一个总结。1.选择排序-简单选择排序选
- 1、 在Java1.7之前,我们需要通过下面这种方法, 在finally中释放资源,这种方法有点繁琐。 BufferedReader br
- // Create a handler for a click event.button1.Click += delegate(System
- 前言看标题好像很简单的样子,但是针对使用jar包发布SpringBoot项目就不一样了。当你使用tomcat发布项目的时候,上传文件存放会变
- 最近刚写了一个网易客户端首页导航条的动画效果,现在分享出来给大家学习学习。我说一下这个效果的核心原理。下面是效果图: 首先是布局,
- 本文实例讲述了C#针对xml文件转化Dictionary的方法。分享给大家供大家参考。具体实现方法如下:下面是xml文件:<?xml
- 1.类成员与方法的可见性最小化举例:如果是一个private的方法,想删除就删除如果一个public的service方法,或者一个publi
- 用 Environment 类: &
- Java绘图中,显示文字的方法主要有三种:(1)drawString(String str,int x,int y):在指定的位置显示字符串
- 下面展示一下FTP软件上传功能的过程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下1、上传前上传前选择好要将文件或文件夹上传到远程FTP
- java LRU(Least Recently Used )详解LRU是Least Recently Used 的缩写,翻译过来就是“最近最
- 菜单控制:可以用来判断这个用户是不是有这些角色,没有的话就不展示数据控制:由于数据都是从后端查的,在后端控制权限就可以了<!-- &n
- 手把手教你用C#开发Android应用程序的方法和流程摘要:用C#能开发RFID-android吗?C#真的能开发android程序吗?C#
- 以前使用HttpServletResponse可以通过输出流的方式来向前台输出图片。现在大部分都是使用springboot,在使用sprin
- C sharp (#) 数据类型获取这里研究一下关于c#中如何获取变量类型的问题。首先我们研究一下如何获取单个变量的类型// 问题一:获取单
- 本文为大家分享了AForge实现C#摄像头视频录制功能的具体方法,供大家参考,具体内容如下1. 概述最近由于兴趣学习了下在C#上使用AFor
- SpringBoot启动类静态资源路径SpringBoot核心配置类SpringBoot核心JAR包--》spring-boot-autoc