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

Flutter中如何为ClipPath绘制的半圆添加边框

Flutter票证组件半圆边框实现问题

我是Flutter新手,正在开发票证组件。找到一款票证Widget后,参考Stack Overflow写了代码,用ClipPath成功画出了半圆,但没法给这个半圆加边框。需要添加边框的半圆效果如下:
需要添加边框的半圆

我的参考代码:

import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as v_math;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        body: SafeArea(
          child: MyHomePage(),
        ),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
      ClipPath(
        clipper: DolDurmaClipper(right: 40, holeRadius: 20),
        child: Container(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.all(
              Radius.circular(15),
            ),
            color: Colors.blueAccent,
          ),
          width: 300,
          height: 95,
          padding: EdgeInsets.all(15),
          child: Text('first example'),
        ),
      ),
      SizedBox(
        height: 20,
      ),
      ClipPath(
        clipper: DolDurmaClipper(right: 100, holeRadius: 40),
        child: Container(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.all(
              Radius.circular(15),
            ),
            color: Colors.amber,
          ),
          width: 200,
          height: 250,
          padding: EdgeInsets.all(35),
          child: Text('second example'),
        ),
      ),
    ]);
  }
}

class DolDurmaClipper extends CustomClipper<Path> {
  DolDurmaClipper({@required this.right, @required this.holeRadius});

  final double right;
  final double holeRadius;

  @override
  Path getClip(Size size) {
    final path = Path()
      ..moveTo(0, 0)
      ..lineTo(size.width - right - holeRadius, 0.0)
      ..arcToPoint(
        Offset(size.width - right, 0),
        clockwise: false,
        radius: Radius.circular(1),
      )
      ..lineTo(size.width, 0.0)
      ..lineTo(size.width, size.height)
      ..lineTo(size.width - right, size.height)
      ..arcToPoint(
        Offset(size.width - right - holeRadius, size.height),
        clockwise: false,
        radius: Radius.circular(1),
      );

    path.lineTo(0.0, size.height);

    path.close();
    return path;
  }

  @override
  bool shouldReclip(DolDurmaClipper oldClipper) => true;
}

解决方法

直接给Container加border会被ClipPath裁剪掉,所以得用Stack把裁剪后的容器和绘制边框的CustomPaint叠加,让边框路径和裁剪路径完全匹配。另外原代码里的半圆绘制参数也有问题,一起修复了:

修改后的完整代码

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        body: SafeArea(
          child: MyHomePage(),
        ),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({super.key});

  final clipper1 = DolDurmaClipper(right: 40, holeRadius: 20);
  final clipper2 = DolDurmaClipper(right: 100, holeRadius: 40);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Stack(
          children: [
            ClipPath(
              clipper: clipper1,
              child: Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.all(
                    Radius.circular(15),
                  ),
                  color: Colors.blueAccent,
                ),
                width: 300,
                height: 95,
                padding: EdgeInsets.all(15),
                child: const Text('first example'),
              ),
            ),
            CustomPaint(
              size: const Size(300, 95),
              painter: BorderPainter(
                  clipper: clipper1,
                  borderColor: Colors.black,
                  borderWidth: 2),
            ),
          ],
        ),
        const SizedBox(
          height: 20,
        ),
        Stack(
          children: [
            ClipPath(
              clipper: clipper2,
              child: Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.all(
                    Radius.circular(15),
                  ),
                  color: Colors.amber,
                ),
                width: 200,
                height: 250,
                padding: EdgeInsets.all(35),
                child: const Text('second example'),
              ),
            ),
            CustomPaint(
              size: const Size(200, 250),
              painter: BorderPainter(
                  clipper: clipper2,
                  borderColor: Colors.black,
                  borderWidth: 2),
            ),
          ],
        ),
      ],
    );
  }
}

class DolDurmaClipper extends CustomClipper<Path> {
  DolDurmaClipper({required this.right, required this.holeRadius});

  final double right;
  final double holeRadius;

  @override
  Path getClip(Size size) {
    final path = Path()
      ..moveTo(0, 0)
      ..lineTo(size.width - right - holeRadius, 0.0)
      ..arcToPoint(
        Offset(size.width - right, 0),
        clockwise: false,
        radius: Radius.circular(holeRadius), // 修复:用holeRadius代替1,正确绘制半圆
      )
      ..lineTo(size.width, 0.0)
      ..lineTo(size.width, size.height)
      ..lineTo(size.width - right, size.height)
      ..arcToPoint(
        Offset(size.width - right - holeRadius, size.height),
        clockwise: false,
        radius: Radius.circular(holeRadius), // 同样修复这里
      );

    path.lineTo(0.0, size.height);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(DolDurmaClipper oldClipper) => 
      right != oldClipper.right || holeRadius != oldClipper.holeRadius;
}

class BorderPainter extends CustomPainter {
  final DolDurmaClipper clipper;
  final Color borderColor;
  final double borderWidth;

  BorderPainter({
    required this.clipper,
    required this.borderColor,
    required this.borderWidth,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = borderColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = borderWidth;

    final path = clipper.getClip(size);
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(BorderPainter oldDelegate) => 
      borderColor != oldDelegate.borderColor || 
      borderWidth != oldDelegate.borderWidth ||
      clipper.right != oldDelegate.clipper.right ||
      clipper.holeRadius != oldDelegate.clipper.holeRadius;
}

关键改动点

  1. 修复半圆绘制错误:原代码arcToPoint里的radius用了Radius.circular(1),改成holeRadius才能画出指定大小的半圆。
  2. 叠加边框绘制:用Stack把裁剪后的容器和CustomPaint放在一起,CustomPaint复用裁剪器的路径来画边框,保证形状完全匹配。
  3. 优化重绘逻辑:修改shouldReclip和shouldRepaint,只有当参数变化时才重绘,避免不必要的性能消耗。

内容的提问来源于stack exchange,提问作者user20528344

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 20:35:27