去年用Objective-C
将 potato04 老哥实现的 Swift
版本的 在 iOS 中实现谷歌灭霸彩蛋 仿写了一遍,现在刚自学 Flutter
,现在献上 Flutter
版本的实现;
. | . |
---|---|
![]() |
![]() |
由于是模拟器录制,所以录制的fps
有点低(最高12fps), 更清晰的请参阅沸点
# 响指动画
参阅上篇文章Flutter中使用sprites精灵图的做法,这里直接贴出代码部分;
绘制精灵图的代码:
// animatable_sprite.dart
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class AnimatableSprite extends StatelessWidget {
final ui.Image img;
final int showIndex;
const AnimatableSprite({
Key key,
@required this.img,
this.showIndex,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: CustomPaint(
painter: _SpritesPainter(
img,
showIndex: showIndex,
),
),
);
}
}
class _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.height.toDouble();
Rect src = Rect.fromLTRB(
_showIndex * showSize,
0,
(_showIndex + 1) * showSize,
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;
}
}
控制动画的部分代码
// thanos_gauntlet.dart
import 'package:audioplayers/audio_cache.dart';
import 'package:flutter/material.dart';
import 'package:thanos_snap_flutter/aniamte/animatable_sprite.dart';
import 'dart:ui' as ui;
enum ThanosGauntletAction {
snap,
reverse,
}
class _ThanosGauntletState extends State<ThanosGauntlet>
with TickerProviderStateMixin {
ui.Image snapImg;
ui.Image reverseImg;
AnimationController snapController;
Animation snapAnimation;
...
_initAnimation() {
snapController = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
snapAnimation = IntTween(
begin: 0,
end: snapCount,
).animate(snapController)
..addListener(_handleAnimationChange)
..addStatusListener(_handleAnimationStatus);
...
}
_handleAnimationChange() {
setState(() {
// do nothing
});
}
_handleAnimationStatus(AnimationStatus status) {
// nofity status
...
}
@override
Widget build(BuildContext context) {
if (snapImg != null && reverseImg != null) {
return GestureDetector(
child: Container(
height: 40,
width: 40,
child: AnimatableSprite(
img: !showSnap ? snapImg : reverseImg,
showIndex: !showSnap ? snapAnimation.value : reverseAnimation.value,
),
),
onTap: _showAnimation,
);
} else {
return Container();
}
}
}
就是利用_aniation.value
来不停地更新 AnimatableSprite
的绘制区域以达到动画的效果;
# 沙化消失
核心思想还是将图片处理为像素,然后根据处理后的像素再生成 ui.Image
使用 canvas
进行绘图操作;
像素化处理
新建 dust_effect_draw.dart
文件;
/// 拆解图像
_initImages(ui.Image image) async {
ui.Image originImg = image;
int imageWidth = originImg.width;
ByteData byteData = await originImg.toByteData();
Uint8List originList = new Uint8List.view(byteData.buffer);
int length = originList.length;
// RGBA信息
List<Uint8List> framePixels = new List(imageCount);
for (int i = 0; i < imageCount; i++) {
framePixels[i] = Uint8List(length);
}
// 遍历 originList
for (int idx = 0; idx < length; idx++) {
// 注释 1
if (idx % 4 == 0 && idx > 3) {
double column = (idx / 4) % imageWidth;
// 每个循环2次
for (int i = 0; i < 2; i++) {
// 注释 2
double factor = Random().nextDouble() + 2 * (column / imageWidth);
int index = (imageCount * (factor / 3)).floor();
Uint8List tmp = framePixels[index];
tmp[idx - 1] = originList[idx - 1];
tmp[idx - 2] = originList[idx - 2];
tmp[idx - 3] = originList[idx - 3];
tmp[idx - 4] = originList[idx - 4];
}
}
}
List<DustEffectModel> imageList = List();
for (var e in framePixels) {
ui.Image outputImage;
if (widget.rebuildHeader) {
// 注释 3
Bitmap bitmap = Bitmap.fromHeadless(
originImg.width,
originImg.height,
e,
);
outputImage = await bitmap.buildImage();
} else {
outputImage = await ImageLoader.loader.loadImageByUint8List(e);
}
DustEffectModel model = DustEffectModel(outputImage);
imageList.add(model);
}
setState(() {
this.dustModelList = imageList;
});
}
注释 1: 在我们通过ui.Image
生成的 Unit8List
数组中,每个存储一个 0~255
的数字来表示 R
,G
, B
, A
中的一个信息,从 index = 0
开始每 连续的4位
表示一个完整像素的RGBA
信息,
注释 2: 这里的 index
确保图片左边的部分分布在 framePixels
数组的靠前位置,具体请参阅 potato04原文所述;
注释 3: 这里使用了第三方库用以生成正确的 ui.Image
,应为我们直接通过ui.Image
得到的 Uint8List
是缺失图片信息的其内部仅仅存储了image
的像素信息,因此通过Bitmap 来正确的添加一个 RGBA
头部信息,以保证生成正确的图片;
直接调用
ui.Codec codec = await ui.instantiateImageCodec(list, targetWidth: width, targetHeight: height);
ui.FrameInfo frame = await codec.getNextFrame();
会报如下错误:
[VERBOSE-2:ui_dart_state.cc(157)] Unhandled Exception: Exception: Could not instantiate image codec.
#0 _futurize (dart:ui/painting.dart:4419:5)
#1 instantiateImageCodec (dart:ui/painting.dart:1722:10)
#2 _decodeImageFromListAsync (dart:ui/painting.dart:1751:29)
更详细叙述见flutter/issues/44774
封装处理后图片
新建dust_effect_model.dart
并添加以下代码:
class DustEffectModel {
ui.Image image;
DustEffectModel(this.image);
/*
Point get translate {
if (_translate != null) {
return _translate;
}
double radian1 = pi / 12 * (Random().nextDouble() - 0.5);
double random = pi * 2 * (Random().nextDouble() - 0.5);
double transX = 30 * cos(random);
double transY = 15 * sin(random);
double realTransX = transX * cos(radian1) - transY * sin(radian1);
double realTransY = transY * cos(radian1) + transX * sin(radian1);
_translate = Point(realTransX, realTransY);
return _translate;
}
Point _translate;
*/
double get rotation => _rotation;
ui.Path _path;
ui.Path get path {
if(_path != null) return _path;
ui.Path path = ui.Path();
double radian1 = pi / 12 * (Random().nextDouble() - 0.5);
double random = pi * 2 * (Random().nextDouble() - 0.5);
double transX = 30 * cos(random);
double transY = 15 * sin(random);
double realTransX = transX * cos(radian1) - transY * sin(radian1);
double realTransY = transY * cos(radian1) + transX * sin(radian1);
path.moveTo(0, 0);
path.quadraticBezierTo(transX, transY, realTransX, realTransY);
_path = path;
return _path;
}
Point currentPoint(double scale) {
ui.Path totalPath = path;
ui.PathMetrics pms = totalPath.computeMetrics();
ui.PathMetric pm = pms.elementAt(0);
ui.Tangent t = pm.getTangentForOffset(scale);
return Point(t.position.dx, t.position.dy);
}
double _rotation = pi / 12 * (Random().nextDouble() - 0.5);
}
其中:
image
就是像素化处理后生成的ui.Image
;_path
是image
做translate
动画的路径;_rotation
是image
旋转的角度;currentPoint()
是获得_path
上点的位置的
_path
和 _rotation
都是随机生成的代码逻辑与原文逻辑相同不做赘述。currentPoint()
内部使用的全部是 dart:ui
提供的方法,具体解释请参阅官方文档(官方文档真香,Apple
开发者表示羡慕);
进行绘制
@override
void paint(Canvas canvas, Size size) {
Rect rect = Offset(0, 0) & size;
int length = dustList.length;
// 最小刻度
double miniScale = delay / length;
for (var model in dustList) {
int index = dustList.indexOf(model);
// 根据 index 和 传入的 value 来计算 index 对应的 image 的动画进度
double indexStart = value - (miniScale * index);
// 边界值处理
indexStart = indexStart > 0
? (indexStart < duration ? indexStart : duration.toDouble())
: 0.0;
// 计算进度
double showScale = indexStart / duration;
ui.Image image = model.image;
Rect src = Rect.fromLTRB(
0,
0,
image.width.toDouble(),
image.height.toDouble(),
);
double rotation = model.rotation * showScale;
Point translate = model.currentPoint(showScale);
canvas.save();
// 计算画布中心轨迹圆半径
double r = sqrt(pow(size.width, 2) + pow(size.height, 2));
// 计算画布中心点初始弧度
double startAngle = atan(size.height / size.width);
// 计算画布初始中心点坐标
Point p0 = Point(
r * cos(startAngle),
r * sin(startAngle),
);
// 需要旋转的弧度
double xAngle = rotation;
// 计算旋转后的画布中心点坐标
Point px = Point(
r * cos(xAngle + startAngle) - translate.x,
r * sin(xAngle + startAngle) - translate.y,
);
// 先平移画布
canvas.translate((p0.x - px.x) / 2, (p0.y - px.y) / 2);
// 后旋转
canvas.rotate(xAngle);
// 设置透明度
mPaint.color = Color.fromRGBO(0, 0, 0, (1.0 - showScale));
canvas.drawImageRect(image, src, rect, mPaint);
canvas.restore();
}
}
如代码注释所述,根据传入的 value
计算出当前 index
对应图片的动画进度来绘制图片;
最后我投了个懒,复原动画直接使用了 AnimationController
的reverse
进行的🤪
# 完结
核心代码讲解完毕,感兴趣的话,请点赞支持一下吧😉
完整代码见文章顶部;