如何使用Flutter的CustomPaint实现可复用优惠券UI组件
如何使用Flutter的CustomPaint实现可复用优惠券UI组件
嘿,我之前刚好做过类似的优惠券样式,用Flutter的CustomPaint就能轻松做出带撕拉孔的边缘效果,下面是完整的可复用组件代码,还有我整理的关键细节说明,你可以直接拿去用或者按需修改~
核心思路拆解
这个优惠券组件主要分为三个核心部分:
- 左侧红色金额区和右侧白色信息区的弹性布局
- 用
CustomPaint实现的左右两侧撕拉孔(穿孔边缘)效果 - 上下两个圆形缺口,模拟优惠券可撕开的分界点
关键自定义Painter实现
要做出那种类似可撕优惠券的穿孔边缘,我们需要自定义一个PerforatedEdgePainter,它负责在组件的指定边缘绘制一排等间距的小圆孔:
class PerforatedEdgePainter extends CustomPainter { final double holeRadius; final double holeSpacing; final bool isLeftAligned; final Color color; PerforatedEdgePainter({ required this.holeRadius, required this.holeSpacing, required this.isLeftAligned, required this.color, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.fill; final double startX = isLeftAligned ? 0 : size.width; double currentY = holeRadius; while (currentY < size.height) { canvas.drawCircle(Offset(startX, currentY), holeRadius, paint); currentY += holeSpacing; } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } }
逻辑其实很简单:根据isLeftAligned参数决定在左侧还是右侧绘制圆孔,然后沿着垂直方向按指定间距循环绘制圆形即可。把圆孔颜色设为和另一侧背景色一致,就能形成“穿孔可撕”的视觉效果。
完整优惠券组件代码
下面是完整的VoucherWidget组件,包含了布局、金额展示、信息区和所有自定义绘制效果:
import 'package:flutter/material.dart'; class VoucherWidget extends StatelessWidget { const VoucherWidget({super.key}); @override Widget build(BuildContext context) { const double widgetWidth = 380.0; const double widgetHeight = 130.0; const double redPartFlex = 3; const double whitePartFlex = 7; const double totalFlex = redPartFlex + whitePartFlex; const double cutoutRadius = 12.0; // Calculate the position of the dividing line final double dividerPosition = (widgetWidth * redPartFlex) / totalFlex; return Container( width: widgetWidth, height: widgetHeight, child: Stack( children: [ // Main content row Row( children: [ // Red Part Expanded( flex: redPartFlex.toInt(), child: CustomPaint( painter: PerforatedEdgePainter( holeRadius: 5, holeSpacing: 12, isLeftAligned: true, color: Colors.white, ), child: Container( decoration: const BoxDecoration( color: Color(0xFFD70F53), ), child: const Padding( padding: EdgeInsets.only(left: 16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'RM', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, ), ), Text( '10', style: TextStyle( color: Colors.white, fontSize: 64, fontWeight: FontWeight.bold, height: 1.1, ), ), ], ), ), ), ), ), // White Part Expanded( flex: whitePartFlex.toInt(), child: CustomPaint( painter: PerforatedEdgePainter( holeRadius: 5, holeSpacing: 12, isLeftAligned: false, color: Colors.white, ), child: Container( padding: const EdgeInsets.all(16.0), decoration: const BoxDecoration( color: Color(0xFFF2F2F2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ _buildBadge('Paid'), const SizedBox(width: 8), _buildBadge('Used'), ], ), const Spacer(), const Text( 'RM 10 Voucher', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 22, color: Color(0xFF333333), ), ), const SizedBox(height: 4), Text( 'Valid Till 17 Dec 2024', style: TextStyle( color: Colors.grey[600], fontSize: 14, ), ), ], ), ), ), ), ], ), // Top cutout Positioned( top: -cutoutRadius, left: dividerPosition - cutoutRadius, child: CircleAvatar( radius: cutoutRadius, backgroundColor: Colors.white, ), ), // Bottom cutout Positioned( bottom: -cutoutRadius, left: dividerPosition - cutoutRadius, child: CircleAvatar( radius: cutoutRadius, backgroundColor: Colors.white, ), ), ], ), ); } Widget _buildBadge(String text) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: BoxDecoration( color: const Color(0xFFE0E0E0), borderRadius: BorderRadius.circular(12), ), child: Text( text, style: TextStyle( color: Colors.grey[700], fontWeight: FontWeight.w500, ), ), ); } }
可灵活定制的细节点
你可以根据自己的需求快速调整这些参数:
widgetWidth和widgetHeight:修改优惠券的整体尺寸redPartFlex和whitePartFlex:调整左右两侧的宽度比例holeRadius和holeSpacing:改变撕拉孔的大小和间距- 颜色值:替换红色、白色区域的背景色,以及文字颜色
- 金额和文本内容:直接修改
Text组件的内容就能适配不同优惠券
最后,你只需要在页面中直接使用const VoucherWidget()就能渲染出这个优惠券啦~
内容来源于stack exchange
相关产品推荐
相关产品推荐

