我想在我的应用程序中制作动画,在其中加载图像并使其沿着特定路径移动。我画了一条由几个点组成的线,首先我尝试让一个点沿着移动 - 一切正常。
这是我的代码:
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Turtle Maths',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: false,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
final myController = TextEditingController();
int ratio = 10;
double _progress = 0.0;
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 3000), vsync: this);
animation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
setState(() {
_progress = animation.value;
});
});
}
@override
void dispose() {
myController.dispose();
super.dispose();
}
Future<ui.Image> _loadImage(String imagePath) async {
ByteData bd = await rootBundle.load(imagePath);
final Uint8List bytes = Uint8List.view(bd.buffer);
final ui.Codec codec = await ui.instantiateImageCodec(bytes,
targetHeight: 60, targetWidth: 60);
final ui.Image image = (await codec.getNextFrame()).image;
return image;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Turtle Maths")),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: [
AnimatedBuilder(
animation: controller,
builder: (BuildContext context, _) {
return TextField(
controller: myController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
prefixIcon: const Icon(PiSymbol.pi_outline),
hintText: "Coefficient",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: Colors.black,
width: 1,
style: BorderStyle.solid,
),
),
suffixIcon: Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(100, 50),
iconColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
),
onPressed: () {
setState(() {
if (myController.text.isNotEmpty) {
ratio = int.parse(myController.text);
controller.reset();
controller.forward();
}
});
},
child: const Text("RUN")))),
);
},
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"${myController.text}x\u{00B3} + ${myController.text}x\u{00B2} + ${myController.text}x + ${myController.text} = 0",
style: const TextStyle(fontSize: 18)),
),
Expanded(
child: FutureBuilder<ui.Image>(
future: _loadImage("assets/images/turtle.png"),
builder:(BuildContext context, AsyncSnapshot<ui.Image> snapshot){
if (snapshot.connectionState == ConnectionState.done){
return CustomPaint(
painter: MyPainter(constraints.maxWidth / 2,
constraints.maxHeight / 2, ratio, _progress, snapshot.data),
size: Size(constraints.maxWidth, constraints.maxHeight));
}
else{
return CustomPaint(
painter: MyPainter(constraints.maxWidth / 2,
constraints.maxHeight / 2, ratio, _progress),
size: Size(constraints.maxWidth, constraints.maxHeight));
}
}
)),
],
);
},
));
}
}
class MyPainter extends CustomPainter {
ui.Image? image;
double _width = 0;
double _height = 0;
int _ratio = 0;
final double _progress;
MyPainter(width, height, ratio, this._progress, [this.image]) {
_width = width;
_height = height;
_ratio = ratio * 10;
}
@override
void paint(Canvas canvas, Size size) async {
Paint linePaint = Paint()
..strokeWidth = 20
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
var path = getPath();
ui.PathMetrics pathMetrics = path.computeMetrics();
ui.PathMetric pathMetric = pathMetrics.elementAt(0);
final pos = pathMetric.getTangentForOffset(pathMetric.length * _progress);
Path extracted = pathMetric.extractPath(0.0, pathMetric.length * _progress);
linePaint.strokeWidth = 6;
canvas.drawPath(extracted, linePaint);
if (image == null) {
canvas.drawCircle(pos!.position, linePaint.strokeWidth / 2, linePaint);
} else {
canvas.drawImage(image!, pos!.position, linePaint);
}
}
Path getPath() {
return Path()
..moveTo(_width, _height)
..lineTo(_width + _ratio, _height)
..lineTo(_width + _ratio, _height)
..lineTo(_width + _ratio, _height - _ratio)
..lineTo(_width - _ratio, _height - _ratio)
..lineTo(_width - _ratio, _height + _ratio);
}
@override
bool shouldRepaint(covariant MyPainter oldDelegate) {
return oldDelegate._progress != _progress;
}
}
我之前有过空错误,所以我做了一个 if 语句来检查图像是否存在,现在图像在最开始加载,然后我在动画期间看到点,图像仅在最后再次出现。
有人可以解释一下我在这里做错了什么以及正确的方法是什么?预先感谢。
这里的问题是未来建造者的未来是每一帧都被建造的。
future: _loadImage("assets/images/turtle.png")
这里,图像正在每一帧上加载。
来自官方文档https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
管理未来 未来一定是早先获得的,例如在 State.initState、State.didUpdateWidget 或 State.didChangeDependency 期间。在构造 FutureBuilder 时,不得在 State.build 或 StatelessWidget.build 方法调用期间创建它。如果 future 与 FutureBuilder 同时创建,那么每次重建 FutureBuilder 的父级时,异步任务都会重新启动。
一般准则是假设每个构建方法都可以在每帧被调用,并将省略的调用视为优化。
因此,一旦将其替换为在初始化状态下创建的某个 future,您就会显示图像。
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Turtle Maths',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: false,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
final myController = TextEditingController();
int ratio = 10;
double _progress = 0.0;
late Animation<double> animation;
late AnimationController controller;
late Future<ui.Image> _imageFuture; // added this
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 3000), vsync: this);
animation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
setState(() {
_progress = animation.value;
});
});
_imageFuture = _loadImage("assets/images/turtle.png"); // added this
}
@override
void dispose() {
myController.dispose();
super.dispose();
}
Future<ui.Image> _loadImage(String imagePath) async {
ByteData bd = await rootBundle.load(imagePath);
final Uint8List bytes = Uint8List.view(bd.buffer);
final ui.Codec codec = await ui.instantiateImageCodec(bytes,
targetHeight: 60, targetWidth: 60);
final ui.Image image = (await codec.getNextFrame()).image;
return image;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Turtle Maths")),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: [
AnimatedBuilder(
animation: controller,
builder: (BuildContext context, _) {
return TextField(
controller: myController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.pie_chart),
hintText: "Coefficient",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: Colors.black,
width: 1,
style: BorderStyle.solid,
),
),
suffixIcon: Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(100, 50),
iconColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
),
onPressed: () {
setState(() {
if (myController.text.isNotEmpty) {
ratio = int.parse(myController.text);
controller.reset();
controller.forward();
}
});
},
child: const Text("RUN")))),
);
},
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"${myController.text}x\u{00B3} + ${myController.text}x\u{00B2} + ${myController.text}x + ${myController.text} = 0",
style: const TextStyle(fontSize: 18)),
),
Expanded(
child: FutureBuilder<ui.Image>(
future: _imageFuture, //replaced this line
builder: (BuildContext context,
AsyncSnapshot<ui.Image> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return CustomPaint(
painter: MyPainter(
constraints.maxWidth / 2,
constraints.maxHeight / 2,
ratio,
_progress,
snapshot.data),
willChange: true,
size: Size(constraints.maxWidth,
constraints.maxHeight));
} else {
return CustomPaint(
painter: MyPainter(
constraints.maxWidth / 2,
constraints.maxHeight / 2,
ratio,
_progress),
size: Size(constraints.maxWidth,
constraints.maxHeight));
}
})),
],
);
},
));
}
}
class MyPainter extends CustomPainter {
ui.Image? image;
double _width = 0;
double _height = 0;
int _ratio = 0;
final double _progress;
MyPainter(width, height, ratio, this._progress, [this.image]) {
_width = width;
_height = height;
_ratio = ratio * 10;
}
@override
void paint(Canvas canvas, Size size) async {
Paint linePaint = Paint()
..strokeWidth = 20
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
var path = getPath();
ui.PathMetrics pathMetrics = path.computeMetrics();
ui.PathMetric pathMetric = pathMetrics.elementAt(0);
final pos = pathMetric.getTangentForOffset(pathMetric.length * _progress);
Path extracted = pathMetric.extractPath(0.0, pathMetric.length * _progress);
linePaint.strokeWidth = 6;
canvas.drawPath(extracted, linePaint);
if (image == null) {
canvas.drawCircle(pos!.position, linePaint.strokeWidth / 2, linePaint);
} else {
canvas.drawImage(image!, pos!.position, linePaint);
}
}
Path getPath() {
return Path()
..moveTo(_width, _height)
..lineTo(_width + _ratio, _height)
..lineTo(_width + _ratio, _height)
..lineTo(_width + _ratio, _height - _ratio)
..lineTo(_width - _ratio, _height - _ratio)
..lineTo(_width - _ratio, _height + _ratio);
}
@override
bool shouldRepaint(covariant MyPainter oldDelegate) {
return oldDelegate._progress != _progress;
}
}