「所見即所得」Flutter如何實現爆炸動畫?

本篇文章將展示如何使用 Flutter 完成如下動畫效果,本文相關的 Demo 代碼在 pub 上的 explode_view 項目可以找到。

「所見即所得」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),
);
}

}
1234567891011121314151617181920212223242526272829

接著開始實現 ExplodeViewBody , 主要看它的 State 實現, _ExplodeViewState 中主要繼承瞭 State 並混入瞭 TickerProviderStateMixin 用於實現動畫執行的需求。

class _ExplodeViewState extends State 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 _stateController = StreamController.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: [
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: [
for(Particle particle in particles) particle.startParticleAnimation()
],
),
)
);
}

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

}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384

這裡省略瞭部分代碼,省略部分在後面介紹。

首先,在 _ExplodeViewState 中初始化瞭 StreamController 對象,該對象可以通過 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(
begin: 1.0,
end: 0.0,
).animate(animationController);

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

}
12345678910111213141516171819202122232425

之後實現 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),
),
));
}
},
),
);
}
)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667

如上代碼所示,這裡實現瞭四種不同方向的例子移動,通過使用不同的方向值和 offset ,然後根據上面定義的 Tween 對象配置動畫,最後使用瞭圓形形狀的 BoxDecoration 和可變的高度和寬度創建粒子。

這樣就完成瞭 Particle 類的實現,接下來介紹從圖像中獲取顏色的實現。

Future 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;
}
123456789101112131415161718192021222324252627282930313233

如上所示代碼,實現瞭從圖像中獲取指定位置的像素顏色,在 Demo 中使用瞭不同的方法來加載和設置圖像的 bytes(loadSnapshotBytes() 或者 loadImageBundleBytes()),從而獲取顏色數據。

// Loads the bytes of the image and sets it in the img.Image object
Future 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 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 values = imageBytes.buffer.asUint8List();
photo = img.decodeImage(values);
}

123456789101112131415161718192021

現在當我們長按圖像時,就可以進入散射粒子的最終動畫,並執行以下方法開始生成粒子:

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;
});
});
1234567891011121314151617181920212223242526272829303132333435363738

如上代碼所示,這裡使用瞭 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);
}
123

最後,在搖動圖像並創建瞭粒子之後圖像消失,並且調用之前的 startParticleAnimation 方法,這完成瞭在 Flutter 中的圖像爆炸。

「所見即所得」Flutter如何實現爆炸動畫?

最後如下就可以引入 ExplodeView 。

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

Demo 地址: https://github.com/mdg-soc-19/explode-view/blob/master/lib/explode_view.dart

ps:因為不像 Android 上可以獲取 Bitmap 的橫豎坐標上的二維像素點,所以沒辦法實現整個圖片原地爆炸的效果

「所見即所得」Flutter如何實現爆炸動畫?

轉載自:CSDN博主「戀貓de小郭」的文章
原文鏈接:https://blog.csdn.net/ZuoYueLiang/article/details/104353154