# 本文收获与价值
完成代码后您将收获以下界面:
本文涉及到的知识点:
FutureBuilder;CustomPaint与CustomPainter;canvas.drawImageRect,ImageStream等;最终你将能够吧下面的精灵图按需展示:
# 准备工作
备注:本人的写作方式决定这并不是一篇可以直接直接阅读就能获取到全部代码的文章,偏向于鼓励您能实际动手操练一下。
本文是在前作手把手教你实现携程GridNav布局-hotel布局的代码基础上进行的,当然您也可以重开一个项目,即将要做的事情和前文并无关联性,好的让我们开始吧;
必备条件
添加 sub_nav 和相关资源
在
lib同目录下新建images目录,并将下载的精灵图改名为un_ico_subnav.png后放入images;在
lib目录下新建sub_nav文件夹,在sub_nav目录下新建sub_nav_widget.dart和sub_nav_sprites_image.dart文件;在
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) {} , 这里的 Image 为 ui.Image 类型; 而我们在 Flutter 层使用的 Image 需要先转换为 ImageStream 类型,然后后通过其 addlistener()方法,添加 ImageStreamListener 类型的实例,之后在 ImageStreamListener 实例的完成的回调中通过回调函数中 ImageInfo 的 image 属性获得 Canvas 绘制时需要的 ui.Image;( iOS 的 UIImage 与 CGImage ?)
在
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;在
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然后往上翻,查看官方给出的例子;将
// 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; }将
// todo: show SubNavSpritesImage替换为:// todo: modify column alignment: Alignment.center, child: SubNavSpritesImage( showIndex: 2, img: snapshot.data, ),在
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指定的区域;将
// todo: add customPaint替换为如下代码:child: CustomPaint( painter: _SpritesPainter( widget.img, showIndex: widget.showIndex, ), ),添加到
main.dart,在main.dart中将如下代码,添加到GridWidget()下方Container( margin: EdgeInsets.fromLTRB(16, 10, 16, 0), child: SubNavWidget(), ),然后
cmd+s后,F5调试,您将看到如下界面:
试着更改
sub_nav_widget.dart中传入的showIndex,你将看到不同的图标被绘制;好了让我们回到
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热更新后你将看到最终的效果:
感谢您的阅读。
# 彩蛋
最终我找到了降低Android Studio对CPU占用的办法(虽然现在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.