千家信息网

Flutter如何利用Canvas模拟实现微信红包领取效果

发表于:2024-11-11 作者:千家信息网编辑
千家信息网最后更新 2024年11月11日,这篇文章主要介绍了Flutter如何利用Canvas模拟实现微信红包领取效果的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Flutter如何利用Canvas模拟实现微信红
千家信息网最后更新 2024年11月11日Flutter如何利用Canvas模拟实现微信红包领取效果

这篇文章主要介绍了Flutter如何利用Canvas模拟实现微信红包领取效果的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Flutter如何利用Canvas模拟实现微信红包领取效果文章都会有所收获,下面我们一起来看看吧。

效果

最终实现的整体效果如下:

看完效果以后,接下来就带领大家来看看是怎样一步一步实现最终效果的,在正式动手写代码之前,先对整个效果做一个简单的拆分,将其分为五个部分:

  • 点击弹出红包

  • 红包整体布局

  • 金币点击旋转

  • 红包开启动画

  • 结果页面弹出

拆分后如下图所示:

接下来就一步一步来实现。

红包弹出

红包弹出主要分为两部分:从小到大缩放动画、半透明遮罩。很自然的想到了使用 Dialog 来实现,最终也确实使用 Dialog 实现了对应的效果,但是在最后展示结果页的时候出现问题了,因为红包开启与结果展示是同时进行的,结果页在红包下面,使用 Dialog 的话会存在结果页在 Dialog 上面遮住红包的效果,最后使用了 Overlay 在顶层添加一个 Widget 来实现。

创建一个 RedPacket 的 Widget:

class RedPacket extends StatelessWidget {  const RedPacket({Key? key}) : super(key: key);  @override  Widget build(BuildContext context) {    return Center(        child: Container(          width: 0.8.sw,          height: 1.2.sw,          color: Colors.redAccent,        )    );  }}

内容很简单,就是一个居中的宽高分别为 0.8.sw1.2.sw 的 Container,颜色为红色。这里 sw 是代表屏幕宽度,即红包宽度为屏幕宽度的 0.8 倍,高度为屏幕宽度的 1.2 倍。

关于 Flutter 屏幕适配,请参阅:Flutter应用框架搭建之屏幕适配详解

然后点击按钮时通过 Overlay 展示出来, 创建一个 showRedPacket 的方法:

void showRedPacket(BuildContext context){  OverlayEntry entry = OverlayEntry(builder: (context) => RedPacket());  Overlay.of(context)?.insert(entry);}

效果如下:

红包是弹出来了,但因为没有缩放动画,很突兀。为了实现缩放动画,在 Container 上包裹 ScaleTransition 用于缩放动画,同时将 RedPacket 改为 StatefulWidget ,因为使用动画需要用到 AnimationController 传入 SingleTickerProviderStateMixin ,实现如下:

class RedPacket extends StatefulWidget {  const RedPacket({Key? key}) : super(key: key);  @override  State createState() => _RedPacketState();}class _RedPacketState extends State with SingleTickerProviderStateMixin {  late AnimationController scaleController = AnimationController(vsync: this)    ..duration = const Duration(milliseconds: 500)    ..forward();  @override  Widget build(BuildContext context) {    return Container(      color: Color(0x88000000), /// 半透明遮罩      child: Center(          child: ScaleTransition(            scale: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: scaleController, curve: Curves.fastOutSlowIn)),            child: Container(              width: 0.8.sw,              height: 1.2.sw,              color: Colors.redAccent,            ),          )      ),    );  }}

ScaleTransition 设置动画从 0.0 到 1.0 即从无到原本大小,动画时间为 500 毫秒;同时在外层再包裹一层 Container 并为其添加半透明颜色实现半透明遮罩,最终实现效果:

这样就实现了第一部分的功能。

红包布局

标题说了是使用 Canvas 来实现,所以红包布局主要是使用 Canvas 来实现,将前面红包的 Container 换成 CustomPaint, 然后创建 RedPacketPainter 继承自 CustomPainter :

ScaleTransition(  scale: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: scaleController, curve: Curves.fastOutSlowIn)),  child: CustomPaint(    size: Size(1.sw, 1.sh),    painter: RedPacketPainter(),  ),)

考虑到后续动画,这里将画布的大小设置为全屏。红包布局的核心代码就在 RedPacketPainter 里,首先绘制红包的背景,背景分为上下两部分,上部分又由一个矩形和一个圆弧组成,下半部分同样是由一个矩形和一个圆弧组成,上半部分的圆弧是凸出来的,而下半部分的是凹进去的,示意图如下:

初始化:

/// 画笔 late final Paint _paint = Paint()..isAntiAlias = true;/// 路径final Path path = Path();/// 红包的高度:1.2倍的屏幕宽度late double height = 1.2.sw;/// 上半部分贝塞尔曲线的结束点late double topBezierEnd = (1.sh - height)/2 + height/8*7;/// 上半部分贝塞尔曲线的起点late double topBezierStart= topBezierEnd - 0.2.sw;/// 下半部分贝塞尔曲线的起点late double bottomBezierStart = topBezierEnd - 0.4.sw;/// 金币中心点,后续通过path计算Offset goldCenter = Offset.zero;/// 横向的中心点final double centerWidth = 0.5.sw;/// 红包在整个界面的leftlate double left = 0.1.sw;/// 红包在整个界面的rightlate double right = 0.9.sw;/// 红包在整个界面的toplate double top = (1.sh - height)/2;/// 红包在整个界面的bottomlate double bottom = (1.sh - height)/2 + height;

上半部分

代码实现如下:

void drawTop(ui.Canvas canvas) {    path.reset();  path.addRRect(RRect.fromLTRBAndCorners(left, top, right, topBezierStart, topLeft: const Radius.circular(5), topRight: const Radius.circular(5)));  var bezierPath = getTopBezierPath();  path.addPath(bezierPath, Offset.zero);  path.close();  canvas.drawShadow(path, Colors.redAccent, 2, true);  canvas.drawPath(path, _paint);}

这里使用 Path 来进行绘制,首先向路径中添加一个圆角矩形,也就是示意图中的第①部分,然后通过 getTopBezierPath 获取一个贝塞尔曲线的 bezierPath 并将其添加到 path 路径中,getTopBezierPath 源码如下:

Path getTopBezierPath() {  Path bezierPath = Path();  bezierPath.moveTo(left, topBezierStart);  bezierPath.quadraticBezierTo(centerWidth, topBezierEnd, right , topBezierStart);  var pms = bezierPath.computeMetrics();  var pm = pms.first;  goldCenter = pm.getTangentForOffset(pm.length / 2)?.position ?? Offset.zero;  return bezierPath;}

getTopBezierPath 源码分为两部分,第一部分是创建贝塞尔曲线的 path ,使用的是最开始初始化的数据创建,实现示意图中的第②部分内容;然后根据创建好的贝塞尔曲线的 path 计算出路径中中间点的坐标,作为金币中心点坐标。示意图如下:

图中红点就是贝塞尔曲线的点,中间实线就是贝塞尔曲线,也就是上面代码中创建的贝塞尔曲线路径,实线中间的点就是金币位置的中心点。

贝塞尔曲线绘制完成后调用 drawShadow 绘制阴影,作用是突出上下两部分连接处的效果,最后通过 path 绘制出整个上半部分的效果,如下:

下半部分

代码实现如下:

void drawBottom(ui.Canvas canvas) {  path.reset();  path.moveTo(left, bottomBezierStart );  path.quadraticBezierTo(centerWidth, topBezierEnd, right , bottomBezierStart);    path.lineTo(right, topBezierEnd);  path.lineTo(left, topBezierEnd);    path.addRRect(RRect.fromLTRBAndCorners(left, topBezierEnd, right, bottom, bottomLeft: const Radius.circular(5), bottomRight: const Radius.circular(5)));  path.close();    canvas.drawShadow(path, Colors.redAccent, 2, true);    canvas.drawPath(path, _paint);}

下半部分实现同样分为两部分,首先绘制出贝塞尔曲线,即示意图第③部分,然后再添加一个圆角矩形,即示意图第④部分;然后绘制下半部分的阴影和图形,单独展示下半部分效果如下:

将上下两部分结合起来,就实现了红包背景的效果,如下:

金币绘制

背景绘制完成后接下来进行金币的绘制,前面已计算出金币的中心点坐标,静态金币的绘制就相对来说比较简单了,就是一个圆形,代码如下:

void drawGold(ui.Canvas canvas){  Path path = Path();  canvas.save();  canvas.translate(0.5.sw, goldCenter.dy);  _paint.style = PaintingStyle.fill;  path.addOval(Rect.fromLTRB(-40.w , -40.w, 40.w , 40.w));  _paint.color = const Color(0xFFFCE5BF);  canvas.drawPath(path, _paint);  canvas.restore();}

这里将画布移动到到金币的中心点,然后向 Path 中添加添加一个半径为 40.w 的圆,最后将 path 绘制出来即可。效果如下:

金币文字绘制

金币绘制出来后,还需在金币上绘制一个繁体的 "開" 字,代码如下:

void drawOpenText(ui.Canvas canvas) {  if(controller.showOpenText){    TextPainter textPainter = TextPainter(      text: TextSpan(        text: "開",        style: TextStyle(fontSize: 34.sp, color: Colors.black87, height: 1.0, fontWeight: FontWeight.w400)      ),      textDirection: TextDirection.ltr,      maxLines: 1,      textWidthBasis: TextWidthBasis.longestLine,      textHeightBehavior: const TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: false)    )..layout();    canvas.save();    canvas.translate(0.5.sw, goldCenter.dy);    textPainter.paint(canvas, Offset(- textPainter.width / 2, -textPainter.height/2));    canvas.restore();  }}

使用 TextPainter 进行文字的绘制,同样是将画布移动到金币的中心,然后绘制文字,效果如下:

头像和文字

经过上面的绘制,效果已经出来了,但是还差红包封面上的用户头像相关文字,使用 Canvas 同样能实现,但这里并没有使用 Canvas 来实现,而是使用 CoustomPaint 的 child 来实现:

CustomPaint(  size: Size(1.sw, 1.sh),  painter: RedPacketPainter(controller: controller),  child: buildChild(),)Container buildChild() {  return Container(    padding: EdgeInsets.only(top: 0.3.sh),    child: Column(      crossAxisAlignment: CrossAxisAlignment.center,      children: [        Row(          mainAxisAlignment: MainAxisAlignment.center,          children: [            ClipRRect(              borderRadius: BorderRadius.circular(3.w),              child: Image.network("https://p26-passport.byteacctimg.com/img/user-avatar/32f1f514b874554f69fe265644ca84e4~300x300.image", width: 24.w,)),            SizedBox(width: 5.w,),            Text("loongwind发出的红包", style: TextStyle(fontSize: 16.sp, color: Color(              0xFFF8E7CB), fontWeight: FontWeight.w500),)          ],        ),        SizedBox(height: 15.w,),        Text("恭喜发财", style: TextStyle(fontSize: 18.sp, color: Color(          0xFFF8E7CB)),)      ],    ),  );}

CoustomPaint 的 child 允许传入一个 Widget,Widget 的实现就是要显示的头像和文字,代码如上,最终效果如下:

至此红包的整个布局的实现就完成了。

金币旋转

前面完成了红包的静态显示,接下来就看看怎么让红包动起来,首先看看怎么让金币旋转起来。

说到旋转首先想到的就是以金币的中心旋转,可以通过旋转画布的旋转或者 path 的 transform 旋转来实现,但是经过实验使用这种方式能让金币旋转起来,但是做到旋转的立体效果却很复杂。所以最终采用的是使用两个圆在 x 轴上进行一定的偏移,然后压缩圆的宽度来模拟实现旋转效果,示意图如下:

如图所示,绘制两个相同的圆,开始时将两个圆重叠在一起,然后同时压缩圆的宽度并将下层的圆向左偏移一定单位,就形成了旋转的立体效果。

代码:

void drawGold(ui.Canvas canvas){  Path path = Path();  canvas.save();  canvas.translate(0.5.sw, goldCenter.dy);  _paint.style = PaintingStyle.fill;  path.addOval(Rect.fromLTRB(-40.w , -40.w, 40.w , 40.w));  _paint.color = const Color(0xFFE5CDA8);  canvas.drawPath(path, _paint);  _paint.color = const Color(0xFFFCE5BF);  canvas.drawPath(path, _paint);  canvas.restore();}

修改上面绘制金币的代码,设置不同的颜色再绘制一个圆,这样就在同一个位置绘制了两个不同颜色的圆。那么怎么让它动起来呢?可以使用动画,通过动画执行宽度的缩放,是宽度系数从 1 缩放到 0 再从 0 回到 1。因为 CustomPainter 是继承自 Listenable ,而动画也是 Listenable 所以直接将动画与 CustomPainter 结合起来使用更方便。

为了方便统一控制红包的动画,创建一个 RedPacketController,并在里面创建一个控制金币旋转的动画及控制器:

class RedPacketController{  final SingleTickerProviderStateMixin tickerProvider;  late AnimationController angleController;  late Animation angleCtrl;    RedPacketController({required this.tickerProvider}){    initAnimation();  }    void initAnimation() {    angleController = AnimationController(        duration: const Duration(seconds: 3),        vsync: tickerProvider    )..repeat(reverse: true);    angleCtrl = angleController.drive(Tween(begin: 1.0, end: 0.0));  }  void dispose(){    angleController.dispose();    timer?.cancel();  }}

为了看到旋转的效果,将动画执行时间设置为 3 秒,并且让其重复执行,重复执行时设置 reverse 为 true,即反向执行,然后改造 _RedPacketState 混入 SingleTickerProviderStateMixin 并创建 RedPacketController :

class _RedPacketState extends State with SingleTickerProviderStateMixin{  late RedPacketController controller = RedPacketController(tickerProvider: this);    Widget buildRedPacket() {    return GestureDetector(      onTapUp: controller.clickGold,      child: CustomPaint(        size: Size(1.sw, 1.sh),        painter: RedPacketPainter(controller: controller),        child: buildChild(),      ),    );  }  /// ...}class RedPacketPainter extends CustomPainter{  RedPacketController controller;  RedPacketPainter({required this.controller}) : super(repaint:controller.angleController);    /// ...}

RedPacketPainter 的构造方法调用了 super 并传入了 repaint 参数,即创建的动画控制器。

这样就能在绘制金币的时候使用动画的值了:

void drawGold(ui.Canvas canvas){  Path path = Path();  double angle = controller.angleCtrl.value;  canvas.save();  canvas.translate(0.5.sw, goldCenter.dy);  _paint.style = PaintingStyle.fill;  path.addOval(Rect.fromLTRB(-40.w * angle , -40.w, 40.w * angle, 40.w));  _paint.color = const Color(0xFFE5CDA8);  canvas.drawPath(path, _paint);  _paint.color = const Color(0xFFFCE5BF);  canvas.drawPath(path, _paint);  canvas.restore();}

通过 controller.angleCtrl.value 获取当前动画的值,然后在圆的 left 和 right 参数上乘以这个值,看一下效果:

效果已经有了,但是发现在旋转到最小的时候中间是空的,这不符合我们的预期,那怎么办呢?将两个圆的边一一连接起来是不是中间就不空了,如图所示:

代码实现:

void drawGoldCenterRect(ui.Path path, ui.Path path3, ui.Canvas canvas) {  var pms1 = path.computeMetrics();  var pms2 = path3.computeMetrics();  var pathMetric1 = pms1.first;  var pathMetric2 = pms2.first;  var length = pathMetric1.length;  Path centerPath = Path();  for(int i = 0; i 

使用 Path.computeMetrics 计算两个 path 的路径点,循环路径每一个点,将两个 path 的每一个点连接起来然后绘制出来,再来看一下效果:

效果好多了,但是仔细观察发现还是有一个问题,金币看着不是旋转的而是左右摇摆的,这是因为实现的立体的效果一直在一边导致的,需要根据旋转的时机将立体效果的方向切换,从 1 到 0 时在右边,从 0 到 1 时在左边,通过动画的状态进行判断,修改代码如下:

var frontOffset = 0.0;var backOffset = 0.0;if(controller.angleCtrl.status == AnimationStatus.reverse){  frontOffset = 4.w;  backOffset = -4.w;}else if(controller.angleCtrl.status == AnimationStatus.forward){  frontOffset = -4.w;  backOffset = 4.w;}var path3 = path.shift(Offset(backOffset * (1 - angle),  0));path = path.shift(Offset(frontOffset * (1 - angle), 0));

再来看一下效果:

这样旋转效果就很完美了,金币中间还缺一个空心的矩形,实现很简单,在圆的 path 路径中叠加一个矩形即可:

path.addRect(Rect.fromLTRB(-10.w * angle , -10.w, 10.w * angle , 10.w));path.fillType = PathFillType.evenOdd;

设置fillTypeevenOdd ,这样就形成了中心空心的效果,并且由于上面连接两个圆的路径点,这个空心也自带了立体效果,如图:

最后为金币添加点击事件,点击时开启旋转,并隐藏金币上的文字。点击事件可以直接给 CustomPaint 包裹一个 GestureDetector ,点击时判断点击坐标是否在金币的绘制范围内,可以使用 Path.contains 进行判断,所以需要保存金币的 path 用于点击判断,这里将其保存到 controller 里:

controller.goldPath = path.shift(Offset(0.5.sw, goldCenter.dy));

然后在 RedPacketController 里定义对应的变量和方法:

class RedPacketController{  Path? goldPath;  bool showOpenText = true;    bool checkClickGold(Offset point){    return  goldPath?.contains(point) == true;  }    void clickGold(TapUpDetails details) {    if(checkClickGold(details.globalPosition)){      angleController.repeat(reverse: true);      tickerProvider.setState(() {        showOpenText = false;      });    }  }   /// ...}

goldPath 用于保存金币的 path,showOpenText 用于是否显示金币上的文字,点击时判断事件触发点是否在金币范围内,在金币范围内则触发动画启动,并设置金币上的文字不显示。

为 CustomPaint 添加点击事件:

Widget buildRedPacket() {  return GestureDetector(    onTapUp: controller.clickGold,    child: CustomPaint(      size: Size(1.sw, 1.sh),      painter: RedPacketPainter(controller: controller),      child: buildChild(),    ),  );}

看一下效果 :

红包开启

红包开启其实就是将红包上下两部分分别进行向上和向下的平移,再加上背景颜色的渐变实现,示意图如下:

加上之前金币的动画,存在多个动画控制器,所以需要将 _RedPacketState 修改为混入 TickerProviderStateMixin:

class _RedPacketState extends State with TickerProviderStateMixin{  /// ...}

然后在 RedPacketController 添加平移动画控制器,并添加平移和颜色渐变动画,平移系数从 0 到 1, 颜色渐变从不透明到完全透明。并将平移动画与之前的金币动画合并为 repaint

class RedPacketController{  final TickerProviderStateMixin tickerProvider;  late AnimationController angleController;  late AnimationController translateController;  late Animation translateCtrl;  late Animation colorCtrl;      void initAnimation() {    /// ...            translateController = AnimationController(        duration: const Duration(milliseconds: 800),        vsync: tickerProvider    );    translateCtrl = translateController.drive(Tween(begin: 0.0, end: 1.0));    colorCtrl = translateController.drive(ColorTween(        begin: Colors.redAccent,        end: const Color(0x00FF5252))    );    repaint = Listenable.merge([angleController, translateController]);  }}

再在 RedPacketPainter 的 super 里传入 repaint:

RedPacketPainter({required this.controller}) : super(repaint:controller.repaint);

改造绘制红包上半部分代码:

void drawTop(ui.Canvas canvas) {  canvas.save();  canvas.translate(0, topBezierEnd  * ( - controller.translateCtrl.value));  /// ...  canvas.restore();}

添加画布平移操作,平移的 Y 值为上半部分高度乘以动画值,即从 0 向上移动上半部分高度。

下半部分添加同样的处理,平移方向向下:

void drawBottom(ui.Canvas canvas) {  canvas.save();  canvas.translate(0, topBezierStart * (controller.translateCtrl.value));        /// ...  canvas.restore();}

效果如下:

背景的平移效果实现了,但是上面的头像和文字没动,接下来给头像和文字的 Widget 添加 AnimatedBuilder 使用相同的动画让其跟着移动:

Widget buildChild() {    return AnimatedBuilder(      animation: controller.translateController,      builder: (context, child) => Container(        padding: EdgeInsets.only(top: 0.3.sh * (1 - controller.translateCtrl.value)),        child: Column(...),      ),    );  }

通过动态修改 paddingTop 的值,让头像与文字也向上平移。效果如下:

最后在金币点击事件上添加一个定时器,金币旋转 2 秒后执行红包开启动画:

void clickGold(TapUpDetails details) {  if(checkClickGold(details.globalPosition)){    if(angleController.isAnimating){      stop();    }else{      angleController.repeat(reverse: true);      tickerProvider.setState(() {        showOpenText = false;      });      timer = Timer(const Duration(seconds: 2), (){        stop();      });    }  }}void stop() async{  if(angleController.isAnimating){      ///停止金币动画,让动画看起来更自然    if(angleController.status == AnimationStatus.forward){      await angleController.forward();      angleController.reverse();    }else if(angleController.status == AnimationStatus.reverse){      angleController.reverse();    }    tickerProvider.setState(() {      showOpenBtn = false;    });    translateController.forward();  }}

这样就实现了点击金币后,金币旋转 2 秒后开启红包。

结果弹出

结果页是一个新的界面,在红包开启时同步执行,并且拥有一个渐变动画,路由跳转时添加动画实现,代码如下:

void onOpen(){  Navigator.push(    context,    PageRouteBuilder(      transitionDuration: const Duration(seconds: 1),      pageBuilder: (context, animation, secondaryAnimation) =>      FadeTransition(        opacity: animation,        child: const ResultPage(),      )    )  );}

RedPacket 添加一个红包开启时的回调和红包动画完成时的回调,前者用于跳转结果页,后者用于移除 Overlay

OverlayEntry? entry;void showRedPacket(BuildContext context, Function? onOpen){  entry = OverlayEntry(builder: (context) => RedPacket(onFinish: _removeRedPacket, onOpen: onOpen,));  Overlay.of(context)?.insert(entry!);}void _removeRedPacket(){  entry?.remove();  entry = null;}

在金币旋转动画停止时调用:

void stop() async{  if(angleController.isAnimating){     /// ...    translateController.forward();    onOpen?.call();  }}

红包动画完成时调用 onFinish 回调, 给红包最后的平移动画添加监听来实现:

translateController.addStatusListener((status) {  if(status == AnimationStatus.completed){    onFinish?.call();  }});

OK,大功告成,再看看最终的效果:

关于"Flutter如何利用Canvas模拟实现微信红包领取效果"这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对"Flutter如何利用Canvas模拟实现微信红包领取效果"知识都有一定的了解,大家如果还想学习更多知识,欢迎关注行业资讯频道。

红包 效果 动画 金币 部分 代码 文字 曲线 贝塞 贝塞尔 平移 宽度 路径 两个 半部 示意图 结果 就是 颜色 中心点 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 淘宝数据库技术架构 龙之谷 服务器版本号 qq服务器断开怎么回事 资阳gpu服务器 无锡光学系统设计软件开发 网络安全教育学习记录 视频监控软件开发公司有哪些 广州数字引力网络技术有限公司 网络安全法基本法吗 网络安全活动相关信息 nfs存储服务器搭建 建立区块链数据库 供应工业级串口服务器公司 玄武区第三方软件开发信息推荐 海南服务器包装找哪家 数据库异地容灾 免费服务器平台 企业管控软件开发有什么优点 状态的概念 网络安全 互联网科技考试题 浙江大学网络安全学院招生简章 不能在安全服务器上玩游戏的可能 台式电脑网络安全下使用可以吗 网络安全信息的心得体会 苹果手机天气服务器崩溃 昌平区数据网络技术质量保证 软件开发岗位关键素质点标准定义 软件开发和软件培训讲师谁好 海军软件开发标准 互联网科技考试题
0