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;
  }
}
相关推荐
亚洲小炫风2 小时前
Melos 发布pub.dev
flutter·pub.dev
东东爱编码2 小时前
一路磕磕绊绊解决flutter doctor 报错CocoaPods not installed
flutter·xcode·cocoapods
louisgeek4 小时前
Flutter 简介
flutter
JarvanMo15 小时前
关于Flutter架构的小小探讨
前端·flutter
顾林海16 小时前
Flutter 图标和按钮组件
android·开发语言·前端·flutter·面试
yzwdzkn17 小时前
解决Flutter 2.10.5在升级Xcode 16后的各种报错
flutter·macos·xcode
亚洲小炫风19 小时前
flutter json解析增强
flutter·json·json兼容格式
SY.ZHOU19 小时前
Flutter 与原生通信
android·flutter·ios
恋猫de小郭1 天前
IntelliJ IDEA 2025.1 发布 ,默认 K2 模式 | Android Studio 也将跟进
android·前端·flutter