V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ezshine
V2EX  ›  程序员

手写一个在 Flutter 里展示”精灵图“的 Widget

  •  
  •   ezshine ·
    ezshine · 2021-04-28 11:51:34 +08:00 · 1628 次点击
    这是一个创建于 1308 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    之前用 Flutter 里的游戏引擎 Flare 做了一个“是男人就坚持 100 秒”的游戏

    使用 Flare 引擎之后,完全没有了Flutter应用特有的代码风格。虽然更适应我这类有过游戏开发经验的开发者,但并不利于我们学习Flutter框架。所以我在那篇文章最后也说了,要抽空用 Widget 重写一次这个游戏。

    首要任务,就是得有一个支持”精灵图“的Widget,既然是学习,那就不能用别人开发好的,必须得自己亲手造轮子。

    什么是”精灵图“

    image.png

    精灵图的英文是spritesheet(精灵表单),就是在一张图上放置多个图形,只需要加载到内存里一次。在展示的时候,仅展示单个图形的区域。一般多个图形多用来放置连续动画的多个关键帧。除了在游戏引擎里很常见以外,为了减少 web 请求,在前端领域也很常见。

    原理拆解

    加载一张大图,但每次只展示图片的特定区域

    image.png

    比如这张飞机的精灵图,尺寸是 330x82 (像素),横向排布 5 个画面,那么单个画面的尺寸就是330/5 = 66。我们每次展示的区域为x=66*画面序号,y=0,width=66,height=82

    可以设定横向排布或纵向排布

    精灵图可以横向或纵向排布,有些游戏引擎的贴图最大尺寸为 4096x4096,所以还有些情况是需要我们换行切换的,但原理差异并不大,这里就不过多讨论了。

    可以设定播放时间间隔,自动切换多个连续区域

    2021-04-08 09_45_16.gif

    大部分时候我们是需要用精灵图来展示动画的,比如这个飞机的精灵图。其中第 1,2 幅画面用于展示飞机飞行状态的动画,需要循环播放。

    2021-04-08 09_48_43.gif

    第 3,4,5 幅画面用于展示飞机爆炸的动画,只需播放一次。

    思考应该用哪些 Widget 来搭建

    通过一个动画演示来看看我们需要哪些 Widget

    2021-04-08 10_00_37.gif

    • 可以控制显示区域的 Widget ( Container )
    • 需要可以指定坐标的 Widget ( Stack+Positioned )

    原理也清楚了,也知道该用什么 Widget,那么接下来的代码就很容易了

    将思路转变为代码

    @override
    Widget build(BuildContext context) {
    return Container(
        width: 66,
        height: 82,
        child: Stack(
          children: [
            Positioned(
              left: 66*currentIndex,
              top: 0,
              child: widget.image
            )
          ],
        ),
    );
    }
    

    加入定时器,根据设定的时间间隔改变currentIndex,那么图片看上去就动起来了。

    Timer.periodic(widget.duration, (timer) { 
        setState(() {
          if(currentIndex>=4){
            currentIndex=0;
          }
          else currentIndex++;
        });
      }
    });
    

    我们再进一步封装成一个自己原创的Widget,下面是这个 Widget 的全部代码

    import 'dart:async';
    
    import 'package:flutter/widgets.dart';
    
    class AnimatedSpriteImage extends StatefulWidget {
    
      final Image image;
      final Size spriteSize;
      final int startIndex;
      final int endIndex;
      final int playTimes;
      final Duration duration;
      final Axis axis;
    
      AnimatedSpriteImage({
        Key? key,
        required this.image,
        required this.spriteSize,
        required this.duration,
        this.axis = Axis.horizontal,
        this.startIndex = 0,
        this.endIndex = 0,
        this.playTimes = 0,//0 = loop
      }) : super(key: key);
    
      @override
      _AnimatedSpriteImageState createState() => _AnimatedSpriteImageState();
    }
    
    class _AnimatedSpriteImageState extends State<AnimatedSpriteImage> {
    
      int currentIndex = 0;
      int currentTimes = 0;
    
      @override
      void initState() {
    
        currentIndex = widget.startIndex;
    
        Timer.periodic(widget.duration, (timer) { 
          if(currentTimes<=widget.playTimes){
            setState(() {
              if(currentIndex>=widget.endIndex){
                if(widget.playTimes!=0)currentTimes++;
                if(currentTimes<widget.playTimes||widget.playTimes==0)currentIndex=widget.startIndex;
                else currentIndex = widget.endIndex;
              }
              else currentIndex++;
            });
          }
        });
    
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Container(
            width: widget.spriteSize.width,
            height: widget.spriteSize.height,
            
            child: Stack(
              children: [
                Positioned(
                  left: widget.axis==Axis.horizontal?-widget.spriteSize.width*currentIndex:0,
                  top: widget.axis==Axis.vertical?-widget.spriteSize.height*currentIndex:0,
                  child: widget.image
                )
              ],
            ),
        );
      }
    }
    

    封装得好,使用起来也尤其方便。

    //播放飞机飞行状态动画
    AnimatedSpriteImage(
      duration: Duration(milliseconds: 200),//动画的间隔
      image: Image.asset("assets/images/player.png"),//精灵图
      spriteSize: Size(66, 82),//单画面尺寸
      startIndex: 0,//动画起始画面序号
      endIndex: 1,//动画结束画面序号
      playTimes: 0,//播放次数,0 为循环播放
    )
    
    //播放飞机爆炸动画
    AnimatedSpriteImage(
      duration: Duration(milliseconds: 200),//动画的间隔
      image: Image.asset("assets/images/player.png"),//精灵图
      spriteSize: Size(66, 82),//单画面尺寸
      startIndex: 2,//动画起始画面序号
      endIndex: 4,//动画结束画面序号
      playTimes: 1,//播放次数,0 为循环播放
    )
    
    2 条回复    2021-04-29 06:00:35 +08:00
    eurry
        1
    eurry  
       2021-04-28 11:57:17 +08:00
    点赞,干货了,学习一下
    IceDog
        2
    IceDog  
       2021-04-29 06:00:35 +08:00 via iPhone
    学习一下!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1104 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 19:49 · PVG 03:49 · LAX 11:49 · JFK 14:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.