前言
SVGA是一种动画格式,可以兼容安卓、ios和web,可以实现很多复杂的动画,这样开发就不用头疼canvas来实现动画时的卡顿优化问题了,那么接下来简单介绍一下再Flutter中怎么使用svga
引入
首先目前的官方SVGA插件不支持Dart 3x 版本,想要在最新版本使用SVGA需要自己去封装插件实现或者降低Dart版本。
导入地址:pub.dev/packages/sv…
flutter pub add svgaplayer_flutter
// 或者在pubspec.yaml文件中添加
svgaplayer_flutter: ^2.2.0
实现效果展示
如上所示需要播放炸弹动画并且改变动画内部的值,如果按照一般的方案去实现的话,在动图上方放一个文本区域写文本,那么炸弹的膨胀和伸缩过程并不会同步到文字上,需要文字也有效果的话,那么就需要使用svga的更换图层的功能
代码实现
1. 引入svga
import 'package:svgaplayer_flutter/svgaplayer_flutter.dart';
2. 初始化svga controller
// 使用svga需要添加控制器并且需要再当前的类中注入mixin
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
// 引入svga controller
late SVGAAnimationController controller;
int num = 20;
@override
void initState() {
controller = SVGAAnimationController(vsync: this);
// 加载svga
loadAnimation();
super.initState();
}
......
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
// svga区域
child: SizedBox(
width: 300,
height: 300,
child: SVGAImage(controller),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
3. 引入svga资源
void loadAnimation() async {
final videoItem = await SVGAParser.shared.decodeFromAssets("assets/1.svga");
controller.videoItem = videoItem;
controller.repeat();
}
到此,svga已经可以展示在页面上了,如下图所示,整个svga的接入已经完成了,想要实现动态替换上面的文字,
需要使用官方提供的方法controller.videoItem?.dynamicItem.setImage
去实现
4. 分析svga资源
需要动态替换文字,实际上是不停的进行切图,但是我们需要切换的是百分比数字,我们不可能切100张图片然后去检索替换,这样的方案需要UI小姐姐去切100张图,如果真这么做了,那么UI小姐姐应该已经开始磨刀霍霍了。因此,我们需要自己去生成对应的svga替换图片
4.1. 确认图片规格
首先我们需要和制作svga素材的同学预定好需要替换的图层,需要帮我们留好替换的位置图层,及相应的名称,这里推荐我们使用官方推出的预览网站,去查看需要替换的图层名称和规格:
svga.dev/svga-previe…
我们可以看到我们的小姐姐已经帮我们留好了名为zhadan
的图片,并且这个预览网站也告诉了我们图片的大小规格,当然这里也需要和UI小姐姐确认一下最终规格。
4.2. 将文字生成图片流
图片的生成我们使用ui.PictureRecorder()
来借助canvas绘制来完成,具体代码如下:
Future<ui.Image> randerImageByText(
{required String content,
required double fontSize,
Color? color = Colors.white,
required double width,
required double height,
TextAlign textAlign = TextAlign.justify}) async {
var recorder = ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
Paint paint = Paint()..color = Colors.transparent;
TextPainter textPainter = TextPainter(
textAlign: textAlign,
text: TextSpan(
text: content,
style: TextStyle(
color: color,
fontSize: fontSize,
// fontFamily: 'DingTalk',
)),
textDirection: TextDirection.rtl,
textWidthBasis: TextWidthBasis.longestLine,
ellipsis: '...',
maxLines: 1,
)..layout(maxWidth: width, minWidth: width);
double top = (height - textPainter.height) / 2;
canvas.drawRect(
Rect.fromLTRB(0, top, 0 + width, textPainter.height), paint);
// 可以传入minWidth,maxWidth来限制它的宽度,如不传,文字会绘制在一行
textPainter.paint(canvas, Offset(0, top));
Picture picture = recorder.endRecording();
return await picture.toImage(width.toInt(), height.toInt());
}
4.3. 获取到文件流替换图片
我们将获取到的图片流使用setImage
方法更换,这样就形成了我们上面看到的效果
ui.Image? image = await randerImageByText(
content: "$num%",
color: Colors.white,
width: double.parse("90"),
height: double.parse("38"),
fontSize: double.parse("30"),
textAlign: TextAlign.center);
controller.videoItem?.dynamicItem.setImage(image, "zhadan");
全代码展示:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:svgaplayer_flutter/svgaplayer_flutter.dart';
import 'dart:ui' as ui;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late SVGAAnimationController controller;
int num = 20;
@override
void initState() {
controller = SVGAAnimationController(vsync: this);
// myLog("===========${widget.giftUrl}");
loadAnimation();
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
void loadAnimation() async {
final videoItem = await SVGAParser.shared.decodeFromAssets("assets/1.svga");
controller.videoItem = videoItem;
ui.Image? image = await randerImageByText(
content: "$num%",
color: Colors.white,
width: double.parse("90"),
height: double.parse("38"),
fontSize: double.parse("30"),
textAlign: TextAlign.center);
controller.videoItem?.dynamicItem.setImage(image, "zhadan");
controller.repeat();
}
void _incrementCounter() async {
setState(() {
num++;
});
ui.Image? image = await randerImageByText(
content: "$num%",
color: Colors.white,
width: double.parse("90"),
height: double.parse("38"),
fontSize: double.parse("30"),
textAlign: TextAlign.center);
controller.videoItem?.dynamicItem.setImage(image, "zhadan");
}
// 根据文字生成图片流
Future<ui.Image> randerImageByText(
{required String content,
required double fontSize,
Color? color = Colors.white,
required double width,
required double height,
TextAlign textAlign = TextAlign.justify}) async {
var recorder = ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
Paint paint = Paint()..color = Colors.transparent;
TextPainter textPainter = TextPainter(
textAlign: textAlign,
text: TextSpan(
text: content,
style: TextStyle(
color: color,
fontSize: fontSize,
// fontFamily: 'DingTalk',
)),
textDirection: TextDirection.rtl,
textWidthBasis: TextWidthBasis.longestLine,
ellipsis: '...',
maxLines: 1,
)..layout(maxWidth: width, minWidth: width);
double top = (height - textPainter.height) / 2;
canvas.drawRect(
Rect.fromLTRB(0, top, 0 + width, textPainter.height), paint);
// 可以传入minWidth,maxWidth来限制它的宽度,如不传,文字会绘制在一行
textPainter.paint(canvas, Offset(0, top));
Picture picture = recorder.endRecording();
return await picture.toImage(width.toInt(), height.toInt());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: SizedBox(
width: 300,
height: 300,
child: SVGAImage(controller),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
以上便是我在进行项目开发中的一些svga播放的问题处理,希望能够帮到大家。如果各位有更好的方案或者更高级的写法欢迎多多指教
原文链接:https://juejin.cn/post/7355826327356850202 作者:前端李逍