You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:23:05