Flutter创建一个画布自移动图片
最近在设计一个很好看的画布显示图片组件,营造一种高级氛围感,组件效果如下
效果
可以看到图片缓慢的向上移动
原理
通过绘制一个画布来显示一张图片,文字和按钮通过Stack
的组件堆叠来显示
实现步骤
继承CustomPainter
类
创建一个名为DailyTracksCardPainter
的类,继承自CustomPainter
,实现其paint
和shouldRepaint
方法,需要注意的是,此Image
为dart:ui
下的,即 import 'dart:ui' as ui;
dart
// 画布类
class DailyTracksCardPainter extends CustomPainter {
ui.Image? image;
double x;
double y;
DailyTracksCardPainter({this.image, this.x = 0, this.y = 0});
final painter = Paint();
@override
void paint(Canvas canvas, Size size) {
double imageX = image!.width.toDouble();
double imageY = image!.height.toDouble();
// 要绘制的Rect,即原图片的大小
Rect src = Rect.fromLTWH(0, 0, imageX, imageY);
// 要绘制成的Rect,即绘制后的图片大小
canvas.drawImageRect(
image!,
src,
Rect.fromLTWH(x, y, image!.width.toDouble() * size.width / imageX,
image!.height.toDouble() * size.width / imageY),
painter);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
创建StatefulWidget
有状态组件
创建一个名为DailyTracksCard
的组件
dart
class DailyTracksCard extends StatefulWidget {
const DailyTracksCard(
{super.key,
required this.width,
required this.height,
this.defaultTracksList});
final double width;
final double height;
final List<String>? defaultTracksList;
@override
State<DailyTracksCard> createState() => _DailyTracksCardState();
}
class _DailyTracksCardState extends State<DailyTracksCard> {
......
}
创建一个异步获取ui.Image
的方法
dart
Future<ui.Image> loadDailyTracksImage(String path) async {
final data = await NetworkAssetBundle(Uri.parse(path)).load(path);
final bytes = data.buffer.asUint8List();
final image = await decodeImageFromList(bytes);
return image;
}
在此方法中,我们通过网络请求获取图像二进制数据,然后通过decodeImageFromList
方法解码获取图片
创建一个定时器用来定时绘制图片
dart
void timeInit(ui.Image image) {
// 图片播放定时
timer = Timer.periodic(const Duration(milliseconds: 20), (timer) {
double heightImage = image.height * widget.width / image.width;
// 如果加上容器高度大于图片高度
if (-currentY + widget.height >= heightImage) {
direction = true;
} else if (currentY >= 0) {
direction = false;
}
setState(() {
direction ? currentY += 0.3 : currentY -= 0.3;
});
});
}
开启一个定时器,用来不断刷新画布,使其动态显示内容,在判断条件中,如果图片到底部了就让图片向上绘制,反之向下绘制
在初始化中获取图片数据
dart
@override
void initState() {
super.initState();
// 随机从0到3的数,不包括3
int index = Random().nextInt(3).toInt();
image =
loadDailyTracksImage(widget.defaultTracksList![index]).then((value) {
timeInit(value);
return value;
});
}
在本代码中,此image
类型为late Future<ui.Image> image;
,表面他是一个异步变量
通过异步构建组件
dart
FutureBuilder(
future: image,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: DailyTracksCardPainter(
image: snapshot.data,
y: currentY.toDouble(),
),
),
),
const Positioned(
top: 38,
left: 50,
child: Text(
"每日\n推荐",
style: TextStyle(
fontSize: 60,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 12,
),
),
),
Positioned(
right: 30,
bottom: 30,
child: IconButton(
style: ButtonStyle(
// 半透明背景
backgroundColor: MaterialStateProperty.all(
Colors.white.withOpacity(0.15),
),
overlayColor: MaterialStateProperty.all(
Colors.white.withOpacity(0.3),
),
),
// 播放按钮
icon: const Icon(
Icons.play_arrow_rounded,
color: Colors.white,
size: 60,
),
onPressed: () {},
),
)
],
);
}
return const SizedBox();
},
)
自此我们就构建完成
使用组件
dart
DailyTracksCard(
width: 550,
height: 250,
defaultTracksList: const [
'https://p2.music.126.net/0-Ybpa8FrDfRgKYCTJD8Xg==/109951164796696795.jpg',
'https://p2.music.126.net/QxJA2mr4hhb9DZyucIOIQw==/109951165422200291.jpg',
'https://p1.music.126.net/AhYP9TET8l-VSGOpWAKZXw==/109951165134386387.jpg',
],
)
源码
整个程序源码如下
dart
import 'dart:async';
import 'dart:math';
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
class DailyTracksCard extends StatefulWidget {
const DailyTracksCard(
{super.key,
required this.width,
required this.height,
this.defaultTracksList});
final double width;
final double height;
final List<String>? defaultTracksList;
@override
State<DailyTracksCard> createState() => _DailyTracksCardState();
}
class _DailyTracksCardState extends State<DailyTracksCard> {
// 定时器
late Timer timer;
double currentY = 0;
// 播放方向 false 为正向 true 为反向
bool direction = false;
late Future<ui.Image> image;
// 获取For You 每日推荐图片
Future<ui.Image> loadDailyTracksImage(String path) async {
final data = await NetworkAssetBundle(Uri.parse(path)).load(path);
final bytes = data.buffer.asUint8List();
final image = await decodeImageFromList(bytes);
return image;
}
void timeInit(ui.Image image) {
// 图片播放定时
timer = Timer.periodic(const Duration(milliseconds: 20), (timer) {
double heightImage = image.height * widget.width / image.width;
// 如果加上容器高度大于图片高度
if (-currentY + widget.height >= heightImage) {
direction = true;
} else if (currentY >= 0) {
direction = false;
}
setState(() {
direction ? currentY += 0.3 : currentY -= 0.3;
});
});
}
@override
void initState() {
super.initState();
// 随机从0到3的数,不包括3
int index = Random().nextInt(3).toInt();
image =
loadDailyTracksImage(widget.defaultTracksList![index]).then((value) {
timeInit(value);
return value;
});
}
@override
void dispose() {
super.dispose();
timer.cancel();
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
width: widget.width,
height: widget.height,
child: FutureBuilder(
future: image,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: DailyTracksCardPainter(
image: snapshot.data,
y: currentY.toDouble(),
),
),
),
const Positioned(
top: 38,
left: 50,
child: Text(
"每日\n推荐",
style: TextStyle(
fontSize: 60,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 12,
),
),
),
Positioned(
right: 30,
bottom: 30,
child: IconButton(
style: ButtonStyle(
// 半透明背景
backgroundColor: MaterialStateProperty.all(
Colors.white.withOpacity(0.15),
),
overlayColor: MaterialStateProperty.all(
Colors.white.withOpacity(0.3),
),
),
// 播放按钮
icon: const Icon(
Icons.play_arrow_rounded,
color: Colors.white,
size: 60,
),
onPressed: () {},
),
)
],
);
}
return const SizedBox();
},
),
),
);
}
}
// 画布类
class DailyTracksCardPainter extends CustomPainter {
ui.Image? image;
double x;
double y;
DailyTracksCardPainter({this.image, this.x = 0, this.y = 0});
final painter = Paint();
@override
void paint(Canvas canvas, Size size) {
double imageX = image!.width.toDouble();
double imageY = image!.height.toDouble();
// 要绘制的Rect,即原图片的大小
Rect src = Rect.fromLTWH(0, 0, imageX, imageY);
// 要绘制成的Rect,即绘制后的图片大小
canvas.drawImageRect(
image!,
src,
Rect.fromLTWH(x, y, image!.width.toDouble() * size.width / imageX,
image!.height.toDouble() * size.width / imageY),
painter);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}