Flutter中使用sprites精灵图

2,143 阅读4分钟

# 本文收获与价值

  1. 完成代码后您将收获以下界面:

  2. 本文涉及到的知识点:

    • FutureBuilder;
    • CustomPaintCustomPainter;
    • canvas.drawImageRect,ImageStream等;

    最终你将能够吧下面的精灵图按需展示:

# 准备工作

**备注:**本人的写作方式决定这并不是一篇可以直接直接阅读就能获取到全部代码的文章,偏向于鼓励您能实际动手操练一下。

本文是在前作Flutter一步步实现x程GridNav网格布局_hotel布局与完结的代码基础上进行的,当然您也可以重开一个项目,即将要做的事情和前文并无关联性,好的让我们开始吧;

必备条件
  1. 已有的ctrip_gird_demo工程(或者新建一个工程);

  2. 精灵图下载地址;

    如果失效请在git仓库中下载;

添加 sub_nav 和相关资源
  1. lib 同目录下新建 images 目录,并将下载的精灵图改名为 un_ico_subnav.png 后放入 images ;

  2. lib 目录下新建 sub_nav 文件夹,在 sub_nav 目录下新建 sub_nav_widget.dartsub_nav_sprites_image.dart 文件;

  3. pubspec.yaml 中添加资源依赖:

    assets:
      - images/un_ico_subnav.png
    

    然后 cmd + s, VSCode 将会运行 flutter packages get 命令;

# 代码部分

flutter 要想精确的自己控制图片的显示效果,必须借助于 dart:ui 提供的 ui.Canvas 来进行自定义的绘制操作,常用方法为 void drawImageRect(Image image, Rect src, Rect dst, Paint paint) {} , 这里的 Imageui.Image 类型; 而我们在 Flutter 层使用的 Image 需要先转换为 ImageStream 类型,然后后通过其 addlistener()方法,添加 ImageStreamListener 类型的实例,之后在 ImageStreamListener 实例的完成的回调中通过回调函数中 ImageInfoimage 属性获得 Canvas 绘制时需要的 ui.Image;( iOSUIImageCGImage ?)

  1. sub_nav_sprites_image.dart 中添加如下代码

    import 'package:flutter/material.dart';
    
    class SubNavSpritesImage extends StatefulWidget {
      final int showIndex;
      final ui.Image img;
    
      SubNavSpritesImage({
        Key key,
        @required this.showIndex,
        @required this.img,
      }) : super(key: key);
    
      @override
      _SubNavSpritesImageState createState() => _SubNavSpritesImageState();
    }
    
    class _SubNavSpritesImageState extends State<SubNavSpritesImage> {
      @override
      Widget build(BuildContext context) {
        return Container(
          width: 28,
          height: 28,
          // todo: add customPaint
        );
      }
    }
    

    这里我们先不急着去实现自定义绘制,让我们先获取 ui.Image;

  2. sub_nav_widget.dart 中添加如下代码:

    import 'package:flutter/material.dart';
    import 'package:ctrip_gird_demo/sub_nav/sub_nav_sprites_image.dart';
    import 'dart:ui' as ui;
    import 'dart:async';// show Completer
    
    class SubNavWidget extends StatelessWidget {
    
      // 图标对应的名称 
      final List<String> _names = [
        '自由行',
        'Wifi电话本',
        '保险·签证',
        '换钞·购物',
        '当地向导',
        '特价机票',
        '礼品卡',
        '申卡·借钱',
        '旅拍',
        '更多',
      ];
      
      // 每行的个数 
      // todo: modify 5
      final int _rowItemsNum = 5;
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder<ui.Image>(
          future: _loadSubNavImageByProvider(
            AssetImage('images/un_ico_subnav.png'),
          ),
          builder: (context, snapshot) {
            return Container(
            	// todo: show SubNavSpritesImage
            );
          },
        );
      }
    
      // todo: reader ui.Image
      Future<ui.Image> _loadSubNavImageByProvider(
        ImageProvider provider, {
        ImageConfiguration config = ImageConfiguration.empty,
      }) async {
        return null;
      }
    
    

    现在,让我们开始通过 AssetImage 来获取 ui.Image,如果您有疑问的话可以按住 cmd 点击 ImageProvider 然后往上翻,查看官方给出的例子;

  3. // todo: reader ui.Image 替换为如下代码:

    Future<ui.Image> _loadSubNavImageByProvider(
      ImageProvider provider, {
      ImageConfiguration config = ImageConfiguration.empty,
    }) async {
      // 异步 需要导入 `dart:async`
      Completer<ui.Image> completer = Completer<ui.Image>(); //完成的回调
      ImageStream stream = provider.resolve(config); //获取图片流
      ImageStreamListener listener;// 先声明
      // 生成一个监听对象,只关心完成的回调,忽略 onError 回调
      listener = ImageStreamListener(
        (ImageInfo frame, bool sync) {
          // 完成后通过 frame 获取到 ui.Image
          final ui.Image image = frame.image;
          completer.complete(image); //完成
          stream.removeListener(listener); //移除监听
        },
        // node: ignore `onError` and `onChunk`.
      );
      stream.addListener(listener); //添加监听
      return completer.future;
    }
    
  4. // todo: show SubNavSpritesImage 替换为:

    // todo: modify column
    alignment: Alignment.center,
    child: SubNavSpritesImage(
      showIndex: 2,
      img: snapshot.data,
    ),
    
  5. sub_nav_sprites_image.dart 中增加自定义绘制的代码:

    lass _SpritesPainter extends CustomPainter {
    
      final ui.Image _img; // 图片
      Paint mainPaint;
    
      int _showIndex = 0;
    
      _SpritesPainter(
        this._img, {
        @required int showIndex,
      }) {
        this._showIndex = showIndex;
        mainPaint = Paint();
      }
    
      @override
      void paint(ui.Canvas canvas, ui.Size size) {
        Rect rect = Offset(0, 0) & size;
        // 裁剪绘制区域
        canvas.clipRect(rect);
        if (_img != null) {
          double showSize = _img.width.toDouble();
          // 计算将要显示的区域
          Rect src = Rect.fromLTRB(
            0,
            _showIndex * showSize,
            showSize,
            (_showIndex + 1) * showSize,
          );
          // src: _img将要显示的区域, rect: _img将要显示的区域实际被绘制的区域
          canvas.drawImageRect(_img, src, rect, mainPaint);
        }
      }
    
      @override
      bool shouldRepaint(CustomPainter oldDelegate) {
        if (oldDelegate is _SpritesPainter) {
          _SpritesPainter oldPainter = oldDelegate;
          if (oldPainter._showIndex != this._showIndex ||
              oldPainter.mainPaint != this.mainPaint) {
            return true;
          }
        }
        return false;
      }
    }
    

    如上,我们自定义了一个 _SpritesPainter 用来绘制ui.Image指定的区域;

  6. // todo: add customPaint 替换为如下代码:

    child: CustomPaint(
      painter: _SpritesPainter(
        widget.img,
        showIndex: widget.showIndex,
      ),
    ),
    
  7. 添加到 main.dart,在 main.dart 中将如下代码,添加到 GridWidget() 下方

    Container(
      margin: EdgeInsets.fromLTRB(16, 10, 16, 0),
      child: SubNavWidget(),
    ),
    

    然后 cmd + s 后,F5 调试,您将看到如下界面:

    试着更改 sub_nav_widget.dart 中传入的 showIndex ,你将看到不同的图标被绘制;

  8. 好了让我们回到 sub_nav_widget.dart 添加如下布局代码:

    Widget _nomalItemExpanded(index, ui.Image img) {
      return Expanded(
        child: _itemContainer(index, img),
        flex: 1,
      );
    }
    
    Widget _itemExpandedWithNew(index, ui.Image img) {
      return Expanded(
        flex: 1,
        child: Stack(
          alignment: Alignment.center,
          children: <Widget>[
            _itemContainer(index, img),
            _newTagText(),
          ],
        ),
      );
    }
    
    Widget _newTagText() {
      return Positioned(
        top: 0,
        child: Container(
          padding: EdgeInsets.fromLTRB(4, 0, 4, 0),
          decoration: BoxDecoration(
            gradient:
                LinearGradient(colors: [Color(0xfff94242), Color(0xffffa25f)]),
            borderRadius: BorderRadius.circular(8),
          ),
          child: Text(
            'NEW',
            style: TextStyle(
              color: Colors.white,
              fontSize: 10,
            ),
          ),
        ),
      );
    }
    
    Widget _itemContainer(index, ui.Image img) {
      return Container(
        height: 55,
        child: Column(
          children: <Widget>[
            SubNavSpritesImage(
              showIndex: index,
              img: img,
            ),
            Text(
              _names[index],
              style: TextStyle(
                color: Color(0xff222222),
                fontSize: 11,
              ),
            ),
          ],
        ),
      );
    }
    
    List<Widget> _rowContentList(int rowIdx, ui.Image img) {
      return List<int>.generate(_rowItemsNum, (i) => i + rowIdx * _rowItemsNum)
          .map((idx) {
        if (idx == 8) {
          return _itemExpandedWithNew(idx, img);
        }
        return _nomalItemExpanded(idx, img);
      }).toList();
    }
    
    Widget _rowItemContent(int rowIdx, ui.Image img) {
      if ((rowIdx + 1) * _rowItemsNum > _names.length) {
        return null;
      }
      return Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: _rowContentList(rowIdx, img),
      );
    }
    

    如上,代码逻辑很简单这里不做过多解释,然后将 // todo: modify column 替换为:

    return Column(
      children: <Widget>[
        _rowItemContent(0, snapshot.data),
        _rowItemContent(1, snapshot.data),
      ],
    );
    

    cmd + s 热更新后你将看到最终的效果:

感谢您的阅读。demo传送门

# 彩蛋

最终我找到了降低Android StudioCPU占用的办法(虽然现在CPU温度还在67°左右徘徊😅),详见:万能的stackoverflow

Go to: Preferences > Version Control > Background. Now listed under 'Background Operations' are 6 options. I disabled the first three options which are:

Perform update on VCS in background, Perform commit to VCS in background, Perform checkout to VCS in background.