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

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

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

效果

请添加图片描述

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

原理

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

实现步骤

继承CustomPainter

创建一个名为DailyTracksCardPainter的类,继承自CustomPainter,实现其paintshouldRepaint方法,需要注意的是,此Imagedart:ui下的,即
import 'dart:ui' as ui;

// 画布类
class DailyTracksCardPainter extends CustomPainter {
 ui.Image? image;
 double x;
 double y;

 DailyTracksCardPainter({this.image, this.x = 0, this.y = 0});

 final painter = Paint();
 
 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);
 }

 
 bool shouldRepaint(covariant CustomPainter oldDelegate) {
   return true;
 }
}

创建StatefulWidget有状态组件

创建一个名为DailyTracksCard的组件

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;

 
 State<DailyTracksCard> createState() => _DailyTracksCardState();
}

class _DailyTracksCardState extends State<DailyTracksCard> {
   ......
}

创建一个异步获取ui.Image的方法

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方法解码获取图片

创建一个定时器用来定时绘制图片

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;
  });
});
}

开启一个定时器,用来不断刷新画布,使其动态显示内容,在判断条件中,如果图片到底部了就让图片向上绘制,反之向下绘制

在初始化中获取图片数据

  
 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;,表面他是一个异步变量

通过异步构建组件

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();
 },
)

自此我们就构建完成

使用组件

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',
 ],
)

源码

整个程序源码如下

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;

  
  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;
      });
    });
  }

  
  void initState() {
    super.initState();
    // 随机从0到3的数,不包括3
    int index = Random().nextInt(3).toInt();
    image =
        loadDailyTracksImage(widget.defaultTracksList![index]).then((value) {
      timeInit(value);
      return value;
    });
  }

  
  void dispose() {
    super.dispose();
    timer.cancel();
  }

  
  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();
  
  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);
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}
Flutter 中,你可以使用 `CustomPainter` 和相关的 Widget 组件来创建一个可以手动旋转的圆形。`CustomPainter` 是一个高级 API,它允许你在绘画上下文中绘制任意复杂的图形,包括动态内容,比如动画。 首先,你需要创建一个 CustomPainter 类,这个类将有一个 paint 方法,里面会包含圆形的绘制逻辑,并且需要重写 `didChangeSize` 来处理视图大小的变化: ```dart import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class RotatingCirclePainter extends CustomPainter { double initialRotation = 0; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue ..style = PaintingStyle.stroke ..strokeWidth = 10.0 ..isAntiAlias = true; // 计算画布中心点 final centerX = size.width / 2.0; final centerY = size.height / 2.0; // 根据手指的位置调整旋转角度 double rotationAngle = initialRotation + Offsettouches.globalPosition.dx * (360.0 / size.width); // 绘制圆形 canvas.translate(centerX, centerY); canvas.rotate(rotationAngle * pi / 180.0); // 将弧度转换成角度 canvas.drawCircle(Offset(0, 0), size.width / 4.0, paint); canvas.restore(); } @override bool shouldRepaint(CustomPainter oldDelegate) => false; // 自定义画家默认不需要重绘 @override void didChangeSize(Size newSize) { super.didChangeSize(newSize); initialRotation = Orientation.isPortrait ? -90 : 0; // 如果是竖屏,初始旋转90度 } } ``` 然后,在 StatefulWidget 中使用 `GestureDetector` 和 `CustomPaint` 来监听手势并应用旋转: ```dart class RotatingCircleWidget extends StatefulWidget { @override _RotatingCircleWidgetState createState() => _RotatingCircleWidgetState(); } class _RotatingCircleWidgetState extends State<RotatingCircleWidget> { RotatingCirclePainter painter = RotatingCirclePainter(); @override Widget build(BuildContext context) { return GestureDetector( onPanUpdate: (details) { setState(() { if (Orientation.isPortrait) { painter.initialRotation += details.delta.dx; } else { painter.initialRotation -= details.delta.dx; } }); }, child: CustomPaint( painter: painter, size: Size.infinite, ), ); } } ``` 当用户触摸屏幕并移动手指时,`onPanUpdate` 函数会被触发,更新 `initialRotation` 属性,进而改变圆形的旋转角度。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值