Android实现点汇聚成字的动态效果详解

 更新时间:2022年7月15日 10:04  点击:762 作者:岛上码农

前言

在引入 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

总结

本篇介绍了点阵的概念,以及基于点阵如何绘制文字、图形,最后通过先绘制随机点,再汇聚成文字的动画效果。可以看到,化整为零,再聚零为整的动画效果还是蛮酷炫的。实际上,基于这种方式,可以构建更多有趣的动画效果。

以上就是Android实现点汇聚成字的动态效果详解的详细内容,更多关于Android点汇聚成字的资料请关注猪先飞其它相关文章!

原文出处:https://juejin.cn/post/7120233450627891237

[!--infotagslink--]

相关文章

  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • Android开发中findViewById()函数用法与简化

    findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20
  • Android模拟器上模拟来电和短信配置

    如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
  • 夜神android模拟器设置代理的方法

    夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
  • android自定义动态设置Button样式【很常用】

    为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
  • Android WebView加载html5页面实例教程

    如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
  • 深入理解Android中View和ViewGroup

    深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
  • Android自定义WebView网络视频播放控件例子

    下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
  • Android用MemoryFile文件类读写进行性能优化

    java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
  • Android设置TextView竖着显示实例

    TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
  • android.os.BinderProxy cannot be cast to com解决办法

    本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20
  • Android 实现钉钉自动打卡功能

    这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
  • Android 开发之布局细节对比:RTL模式

    下面我们来看一篇关于Android 开发之布局细节对比:RTL模式 ,希望这篇文章对各位同学会带来帮助,具体的细节如下介绍。 前言 讲真,好久没写博客了,2016都过了一半了,赶紧...2016-10-02
  • Android中使用SDcard进行文件的读取方法

    首先如果要在程序中使用sdcard进行存储,我们必须要在AndroidManifset.xml文件进行下面的权限设置: 在AndroidManifest.xml中加入访问SDCard的权限如下: <!--...2016-09-20
  • Android开发之PhoneGap打包及错误解决办法

    下面来给各位简单的介绍一下关于Android开发之PhoneGap打包及错误解决办法,希望碰到此类问题的同学可进入参考一下哦。 在我安装、配置好PhoneGap项目的所有依赖...2016-09-20
  • 用Intel HAXM给Android模拟器Emulator加速

    Android 模拟器 Emulator 速度真心不给力,, 现在我们来介绍使用 Intel HAXM 技术为 Android 模拟器加速,使模拟器运行度与真机比肩。 周末试玩了一下在Eclipse中使...2016-09-20
  • Android判断当前屏幕是全屏还是非全屏

    在安卓开发时我碰到一个问题就是需要实现全屏,但又需要我们来判断出用户是使用了全屏或非全屏了,下面我分别找了两段代码,大家可参考。 先来看一个android屏幕全屏实...2016-09-20
  • Android开发中布局中的onClick简单完成多控件时的监听的利与弊

    本文章来为各位介绍一篇关于Android开发中布局中的onClick简单完成多控件时的监听的利与弊的例子,希望这个例子能够帮助到各位朋友. 首先在一个控件加上这么一句:and...2016-09-20
  • Ubuntu 系统下安装Android开发环境 Android Studio 1.0 步骤

    Android Studio 是一个Android开发环境,基于IntelliJ IDEA. 类似 Eclipse ADT,Android Studio 提供了集成的 Android 开发工具用于开发和调试,可以在Linux,Mac OS X,Window...2016-09-20
  • Android实现简单用户注册案例

    这篇文章主要为大家详细介绍了Android实现简单用户注册案例,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-05-26