Flutter创建一个画布自移动图片

Flutter创建一个画布自移动图片

最近在设计一个很好看的画布显示图片组件,营造一种高级氛围感,组件效果如下

效果

可以看到图片缓慢的向上移动

原理

通过绘制一个画布来显示一张图片,文字和按钮通过Stack的组件堆叠来显示

实现步骤

继承CustomPainter

创建一个名为DailyTracksCardPainter的类,继承自CustomPainter,实现其paintshouldRepaint方法,需要注意的是,此Imagedart: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;
  }
}
相关推荐
️ 邪神12 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】文本点击事件
flutter·ios·鸿蒙·reactnative·anroid
️ 邪神13 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】文本Text显示
flutter·ios·鸿蒙·reactnative·anroid
iFlyCai14 小时前
Flutter中有趣的级联语法
flutter
恋猫de小郭14 小时前
Flutter 小技巧之 Shader 实现酷炫的粒子动画
flutter
hello world smile18 小时前
Dart中List API用法大全
flutter·list·dart
lqj_本人1 天前
Flutter&鸿蒙next 使用 BLoC 模式进行状态管理详解
flutter·华为·harmonyos
Miketutu1 天前
flutter 项目初建碰到的控制台报错无法启动问题
flutter
lqj_本人1 天前
flutter&鸿蒙next 使用 InheritedWidget 实现跨 Widget 传递状态
flutter·华为·harmonyos
氤氲息1 天前
flutter 发版的时候设置版本号
flutter
潘敬1 天前
flutter 语法糖库 flutter_magic 发布 1.0.1
开发语言·前端·javascript·flutter·typescript