0
点赞
收藏
分享

微信扫一扫

【译】Flutter:图像的爆炸动画

本篇文章将展示如何使用 Flutter 完成如下动画效果,本文相关的 Demo 代码在 pub 上的 ​​explode_view​​ 项目可以找到。

【译】Flutter:图像的爆炸动画_flutter

首先我们从创建 ​​ExplodeView​​​ 对象开始,该对象在 ​​Widget​​​ 中主要保存 ​​imagePath​​ 和图像的位置。

class ExplodeView extends StatelessWidget {

final String imagePath;

final double imagePosFromLeft;

final double imagePosFromTop;

const ExplodeView({
@required this.imagePath,
@required this.imagePosFromLeft,
@required this.imagePosFromTop
});

@override
Widget build(BuildContext context) {
// This variable contains the size of the screen
final screenSize = MediaQuery.of(context).size;

return new Container(
child: new ExplodeViewBody(
screenSize: screenSize,
imagePath: imagePath,
imagePosFromLeft: imagePosFromLeft,
imagePosFromTop: imagePosFromTop),
);
}

}

接着开始实现 ​​ExplodeViewBody​​​ , 主要看它的 ​​State​​​ 实现, ​​_ExplodeViewState​​​ 中主要继承了 ​​State​​​ 并混入了 ​​TickerProviderStateMixin​​ 用于实现动画执行的需求。

class _ExplodeViewState extends State<ExplodeViewBody> with TickerProviderStateMixin{

GlobalKey currentKey;
GlobalKey imageKey = GlobalKey();
GlobalKey paintKey = GlobalKey();

bool useSnapshot = true;
bool isImage = true;

math.Random random;
img.Image photo;

AnimationController imageAnimationController;

double imageSize = 50.0;
double distFromLeft=10.0, distFromTop=10.0;

final StreamController<Color> _stateController = StreamController<Color>.broadcast();

@override
void initState() {
super.initState();

currentKey = useSnapshot ? paintKey : imageKey;
random = new math.Random();

imageAnimationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 3000),
);

}

@override
Widget build(BuildContext context) {
return Container(
child: isImage
? StreamBuilder(
initialData: Colors.green[500],
stream: _stateController.stream,
builder: (buildContext, snapshot) {
return Stack(
children: <Widget>[
RepaintBoundary(
key: paintKey,
child: GestureDetector(
onLongPress: () async {
//do explode
}
child: Container(
alignment: FractionalOffset((widget.imagePosFromLeft / widget.screenSize.width), (widget.imagePosFromTop / widget.screenSize.height)),
child: Transform(
transform: Matrix4.translation(_shakeImage()),
child: Image.asset(
widget.imagePath,
key: imageKey,
width: imageSize,
height: imageSize,
),
),
),
),
)
],
);
},
):
Container(
child: Stack(
children: <Widget>[
for(Particle particle in particles) particle.startParticleAnimation()
],
),
)
);
}

@override
void dispose(){
imageAnimationController.dispose();
super.dispose();
}

}


这里省略了部分代码,省略部分在后面介绍。


首先,在 ​​_ExplodeViewState​​​ 中初始化了 ​​StreamController<Color>​​​ 对象,该对象可以通过 ​​Stream​​​ 流来控制 ​​StreamBuilder​​ 触发 UI 重绘制。

然后,在 ​​initState​​​ 方法中初始化了 ​​imageAnimationController​​ 作为动画控制器,用于控制图片爆炸前的抖动动画效果。

接着在 ​​build​​​ 方法中, 通过条件判断是需要显示图片还是粒子动画,如果需要显示图像,就使用 ​​Image.asset​​​ 显示图像效果;外层的 ​​GestureDetector​​​ 用于长按时触发爆炸动画效果; ​​StreamBuilder​​​ 中的 ​​stream​​ 用于保存图片的颜色和控制重绘的执行。

接着我们还需要实现 ​​Particle​​ 对象,它被用于配置每个粒子的动画效果。

如下代码所示,在 ​​Particle​​​ 的构造方法中,需要指定 id(Demo 中是 index)、颜色和颗粒的位置作为参数,之后初始化一个 ​​AnimationController​​​ 用于控制粒子的移动效果,通过设置 ​​Tween​​ 来实现动画的在你正负 x 和 y 轴上进行平移,另外还设置了动画过程中颗粒的透明度变化。

Particle({@required this.id, @required this.screenSize, this.colors, this.offsetX, this.offsetY, this.newOffsetX, this.newOffsetY}) {

position = Offset(this.offsetX, this.offsetY);

math.Random random = new math.Random();
this.lastXOffset = random.nextDouble() * 100;
this.lastYOffset = random.nextDouble() * 100;

animationController = new AnimationController(
vsync: this,
duration: Duration(milliseconds: 1500)
);

translateXAnimation = Tween(begin: position.dx, end: lastXOffset).animate(animationController);
translateYAnimation = Tween(begin: position.dy, end: lastYOffset).animate(animationController);
negatetranslateXAnimation = Tween(begin: -1 * position.dx, end: -1 * lastXOffset).animate(animationController);
negatetranslateYAnimation = Tween(begin: -1 * position.dy, end: -1 * lastYOffset).animate(animationController);
fadingAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(animationController);

particleSize = Tween(begin: 5.0, end: random.nextDouble() * 20).animate(animationController);

}

之后实现 ​​startParticleAnimation()​​​ 方法,该方法用于执行粒子动画,该方法通过将上述 ​​animationController​​​ 添加到 ​​AnimatedBuilder​​​ 这个控件中并执行,之后通过​​AnimatedBuilder​​​ 的 ​​builder​​​ 方法配合 ​​Transform​​​ 和 ​​FadeTransition​​, 实现动画的移动和透明度变化效果。

startParticleAnimation() {
animationController.forward();

return Container(
alignment: FractionalOffset(
(newOffsetX / screenSize.width), (newOffsetY / screenSize.height)),
child: AnimatedBuilder(
animation: animationController,
builder: (BuildContext context, Widget widget) {
if (id % 4 == 0) {
return Transform.translate(
offset: Offset(
translateXAnimation.value, translateYAnimation.value),
child: FadeTransition(
opacity: fadingAnimation,
child: Container(
width: particleSize.value > 5 ? particleSize.value : 5,
height: particleSize.value > 5 ? particleSize.value : 5,
decoration:
BoxDecoration(color: colors, shape: BoxShape.circle),
),
));
} else if (id % 4 == 1) {
return Transform.translate(
offset: Offset(
negatetranslateXAnimation.value, translateYAnimation.value),
child: FadeTransition(
opacity: fadingAnimation,
child: Container(
width: particleSize.value > 5 ? particleSize.value : 5,
height: particleSize.value > 5 ? particleSize.value : 5,
decoration:
BoxDecoration(color: colors, shape: BoxShape.circle),
),
));
} else if (id % 4 == 2) {
return Transform.translate(
offset: Offset(
translateXAnimation.value, negatetranslateYAnimation.value),
child: FadeTransition(
opacity: fadingAnimation,
child: Container(
width: particleSize.value > 5 ? particleSize.value : 5,
height: particleSize.value > 5 ? particleSize.value : 5,
decoration:
BoxDecoration(color: colors, shape: BoxShape.circle),
),
));
} else {
return Transform.translate(
offset: Offset(negatetranslateXAnimation.value,
negatetranslateYAnimation.value),
child: FadeTransition(
opacity: fadingAnimation,
child: Container(
width: particleSize.value > 5 ? particleSize.value : 5,
height: particleSize.value > 5 ? particleSize.value : 5,
decoration:
BoxDecoration(color: colors, shape: BoxShape.circle),
),
));
}
},
),
);
}
)

如上代码所示,这里实现了四种不同方向的例子移动,通过使用不同的方向值和 ​​offset​​​ ,然后根据上面定义的 ​​Tween​​​ 对象配置动画,最后使用了圆形形状的 ​​BoxDecoration​​ 和可变的高度和宽度创建粒子。

这样就完成了 ​​Particle​​ 类的实现,接下来介绍从图像中获取颜色的实现。

Future<Color> getPixel(Offset globalPosition, Offset position, double size) async {
if (photo == null) {
await (useSnapshot ? loadSnapshotBytes() : loadImageBundleBytes());
}

Color newColor = calculatePixel(globalPosition, position, size);
return newColor;
}

Color calculatePixel(Offset globalPosition, Offset position, double size) {

double px = position.dx;
double py = position.dy;


if (!useSnapshot) {
double widgetScale = size / photo.width;
px = (px / widgetScale);
py = (py / widgetScale);

}


int pixel32 = photo.getPixelSafe(px.toInt()+1, py.toInt());

int hex = abgrToArgb(pixel32);

_stateController.add(Color(hex));

Color returnColor = Color(hex);

return returnColor;
}

如上所示代码,实现了从图像中获取指定位置的像素颜色,在 Demo 中使用了不同的方法来加载和设置图像的 bytes(​​loadSnapshotBytes()​​​ 或者 ​​loadImageBundleBytes()​​),从而获取颜色数据。

// Loads the bytes of the image and sets it in the img.Image object
Future<void> loadImageBundleBytes() async {
ByteData imageBytes = await rootBundle.load(widget.imagePath);
setImageBytes(imageBytes);
}

// Loads the bytes of the snapshot if the img.Image object is null
Future<void> loadSnapshotBytes() async {
RenderRepaintBoundary boxPaint = paintKey.currentContext.findRenderObject();
ui.Image capture = await boxPaint.toImage();
ByteData imageBytes =
await capture.toByteData(format: ui.ImageByteFormat.png);
setImageBytes(imageBytes);
capture.dispose();
}

void setImageBytes(ByteData imageBytes) {
List<int> values = imageBytes.buffer.asUint8List();
photo = img.decodeImage(values);
}

现在当我们长按图像时,就可以进入散射粒子的最终动画,并执行以下方法开始生成粒子:

RenderBox box = imageKey.currentContext.findRenderObject();
Offset imagePosition = box.localToGlobal(Offset.zero);
double imagePositionOffsetX = imagePosition.dx;
double imagePositionOffsetY = imagePosition.dy;

double imageCenterPositionX = imagePositionOffsetX + (imageSize / 2);
double imageCenterPositionY = imagePositionOffsetY + (imageSize / 2);
for(int i = 0; i < noOfParticles; i++){
if(i < 21){
getPixel(imagePosition, Offset(imagePositionOffsetX + (i * 0.7), imagePositionOffsetY - 60), box.size.width).then((value) {
colors.add(value);
});
}else if(i >= 21 && i < 42){
getPixel(imagePosition, Offset(imagePositionOffsetX + (i * 0.7), imagePositionOffsetY - 52), box.size.width).then((value) {
colors.add(value);
});
}else{
getPixel(imagePosition, Offset(imagePositionOffsetX + (i * 0.7), imagePositionOffsetY - 68), box.size.width).then((value) {
colors.add(value);
});
}
}
Future.delayed(Duration(milliseconds: 3500), () {

for(int i = 0; i < noOfParticles; i++){
if(i < 21){
particles.add(Particle(id: i, screenSize: widget.screenSize, colors: colors[i].withOpacity(1.0), offsetX: (imageCenterPositionX - imagePositionOffsetX + (i * 0.7)) * 0.1, offsetY: (imageCenterPositionY - (imagePositionOffsetY - 60)) * 0.1, newOffsetX: imagePositionOffsetX + (i * 0.7), newOffsetY: imagePositionOffsetY - 60));
}else if(i >= 21 && i < 42){
particles.add(Particle(id: i, screenSize: widget.screenSize, colors: colors[i].withOpacity(1.0), offsetX: (imageCenterPositionX - imagePositionOffsetX + (i * 0.5)) * 0.1, offsetY: (imageCenterPositionY - (imagePositionOffsetY - 52)) * 0.1, newOffsetX: imagePositionOffsetX + (i * 0.7), newOffsetY: imagePositionOffsetY - 52));
}else{
particles.add(Particle(id: i, screenSize: widget.screenSize, colors: colors[i].withOpacity(1.0), offsetX: (imageCenterPositionX - imagePositionOffsetX + (i * 0.9)) * 0.1, offsetY: (imageCenterPositionY - (imagePositionOffsetY - 68)) * 0.1, newOffsetX: imagePositionOffsetX + (i * 0.7), newOffsetY: imagePositionOffsetY - 68));
}
}

setState(() {
isImage = false;
});
});

如上代码所示,这里使用了 ​​RenderBox​​​ 类获得的图像的位置,然后从上面定义的 ​​getPixel()​​ 方法获取颜色。

获取的颜色是从图像上的三条水平线中提取到的,并在同一条线上使用了随机偏移,这样可以从图像中获得到更多的颜色,然后使用适当的参数值在不同位置使用 ​​Particle​​ 创建粒子。

当然这里还有 3.5 秒的延迟执行,而在这个延迟过程中会出现图像抖动。通过使用 ​​Matrix4.translation()​​​ 方法可以简单地实现抖动,该方法使用与下面所示的 ​​_shakeImage​​ 方法来实现不同的偏移量来快速转换图像。

Vector3 _shakeImage() {
return Vector3(math.sin((imageAnimationController.value) * math.pi * 20.0) * 8, 0.0, 0.0);
}

最后,在摇动图像并创建了粒子之后图像消失,并且调用之前的 ​​startParticleAnimation​​ 方法,这完成了在 Flutter 中的图像爆炸。

【译】Flutter:图像的爆炸动画_Flutter_02

最后如下就可以引入 ​​ExplodeView​​ 。

ExplodeView(
imagePath: path,
imagePosFromLeft: xxxx,
imagePosFromTop: xxxx
),

Demo 地址: ​​github.com/mdg-soc-19/…​​


ps:因为不像 Android 上可以获取 Bitmap 的横竖坐标上的二维像素点,所以没办法实现整个图片原地爆炸的效果


资源推荐

  • Github :​​github.com/CarGuo​​
  • 开源 Flutter 完整项目:​​github.com/CarGuo/GSYG…​​
  • 开源 Flutter 多案例学习型项目: ​​github.com/CarGuo/GSYF…​​
  • 开源 Fluttre 实战电子书项目:​​github.com/CarGuo/GSYF…​​
  • 开源 React Native 项目:​​github.com/CarGuo/GSYG…​​

【译】Flutter:图像的爆炸动画_flutter_03

举报

相关推荐

0 条评论