Flutter如何实现如图所示的带嵌入文字的容器边框?
实现带嵌入文字的圆角边框(Flutter)
问题背景
我需要实现Figma设计中那种顶部嵌入文字的圆角边框效果,试过StackOverflow上的几种方案:
- Flutter, how to create border with text embedded?
- Text through border in flutter
但这些方案在添加borderRadius或把顶部边框设为透明时,边框无法正常显示。也尝试过用Paint组件的方案,比如Flutter border around container with text in the middle (divider?),但自己写的代码效果不符合预期。
当前代码
Home组件代码
class Home extends StatefulWidget { const Home({super.key}); @override State<Home> createState() => _HomeState(); } class _HomeState extends State<Home> { final description = 'Lorem ipsum dolor sit amet consectetur. Sed auctor dignissim sit egestas dolor turpis. Nunc congue vulputate tincidunt purus. Non lacus metus tortor elit mauris proin.'; @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; return SingleChildScrollView( child: Column( children: [ FittedBox( child: topSection(header: headerWidget(), body: bodyWidget())), const SizedBox(height: 20), //midSection(), CustomTitleWidget( height: 200, width: double.infinity, title: 'Some of my Projects', radius: 20, ), ], ), ); } }
自定义边框组件代码(效果不符合预期)
class CustomDraw extends CustomPainter { late Paint painter; late double radius; late double textWidth; CustomDraw(Color color, this.textWidth, {this.radius = 0}) { painter = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2 ..color = color; } @override void paint(Canvas canvas, Size size) { var path = Path(); path.moveTo(size.width - ((size.width - textWidth) / 2), 0); path.lineTo(size.width - radius, 0); path.cubicTo(size.width - radius, 0, size.width, 0, size.width, radius); path.lineTo(size.width, size.height - radius); path.cubicTo(size.width, size.height - radius, size.width, size.height, size.width - radius, size.height); path.lineTo(radius, size.height); path.cubicTo(radius, size.height, 0, size.height, 0, size.height - radius); path.lineTo(0, radius); path.cubicTo(0, radius, 0, 0, radius, 0); path.lineTo(((size.width - textWidth) / 2), 0); canvas.drawPath(path, painter); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class CustomTitleWidget extends StatefulWidget { final double height; final double width; final double? radius; final String title; const CustomTitleWidget( {Key? key, required this.height, required this.width, required this.title, this.radius}) : super(key: key); @override State<CustomTitleWidget> createState() => _CustomTitleWidgetState(); } class _CustomTitleWidgetState extends State<CustomTitleWidget> { GlobalKey textKey = GlobalKey(); double textHeight = 0.0; double textWidth = 0.0; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { final textKeyContext = textKey.currentContext; if (textKeyContext != null) { final box = textKeyContext.findRenderObject() as RenderBox; textHeight = box.size.height; textWidth = box.size.width; } }); }); } @override Widget build(BuildContext context) { return Stack( clipBehavior: Clip.none, alignment: Alignment.topCenter, children: [ CustomPaint( painter: CustomDraw( Colors.black, textWidth, radius: widget.radius ?? 0, ), child: SizedBox( height: widget.height, width: widget.width, ), ), Positioned( top: -textHeight / 2, left: 60, child: Padding( key: textKey, padding: EdgeInsets.symmetric(horizontal: 8.0), child: Container( color: Colors.transparent, padding: const EdgeInsets.symmetric(horizontal: 10), child: Text( widget.title, style: AppTextStyles.nameTitle, ), ), ), ) ], ); } }
问题分析与修复方案
核心问题
- 原
CustomDraw路径计算错误,导致顶部边框缺口位置偏移、圆角衔接不自然 - 文字容器背景设为透明,无法覆盖下方边框线,出现文字与边框重叠的情况
- 圆角路径用
cubicTo绘制不规范,容易出现边框缺口
修正后的代码
1. 修复CustomDraw的路径绘制
class CustomDraw extends CustomPainter { final Paint painter; final double radius; final double textWidth; final double strokeWidth; CustomDraw(Color color, this.textWidth, {this.radius = 0, this.strokeWidth = 2}) : painter = Paint() ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..color = color; @override void paint(Canvas canvas, Size size) { final path = Path(); final halfStroke = strokeWidth / 2; final textGapStart = (size.width - textWidth) / 2; final textGapEnd = size.width - textGapStart; // 从文字缺口右侧开始,绘制右上角圆角 path.moveTo(textGapEnd, halfStroke); path.lineTo(size.width - radius, halfStroke); path.arcToPoint( Offset(size.width - halfStroke, radius + halfStroke), radius: Radius.circular(radius), clockwise: false, ); // 绘制右侧边框 path.lineTo(size.width - halfStroke, size.height - radius - halfStroke); path.arcToPoint( Offset(size.width - radius - halfStroke, size.height - halfStroke), radius: Radius.circular(radius), clockwise: false, ); // 绘制底部边框 path.lineTo(radius + halfStroke, size.height - halfStroke); path.arcToPoint( Offset(halfStroke, size.height - radius - halfStroke), radius: Radius.circular(radius), clockwise: false, ); // 绘制左侧边框 path.lineTo(halfStroke, radius + halfStroke); path.arcToPoint( Offset(radius + halfStroke, halfStroke), radius: Radius.circular(radius), clockwise: false, ); // 绘制文字缺口左侧的顶部边框 path.lineTo(textGapStart, halfStroke); canvas.drawPath(path, painter); } @override bool shouldRepaint(covariant CustomDraw oldDelegate) { return oldDelegate.radius != radius || oldDelegate.textWidth != textWidth || oldDelegate.strokeWidth != strokeWidth || oldDelegate.painter.color != painter.color; } }
2. 修复CustomTitleWidget的文字容器
class CustomTitleWidget extends StatefulWidget { final double height; final double width; final double? radius; final String title; final TextStyle? titleStyle; final Color borderColor; final double borderWidth; final Color backgroundColor; const CustomTitleWidget({ Key? key, required this.height, required this.width, required this.title, this.radius, this.titleStyle, this.borderColor = Colors.black, this.borderWidth = 2, this.backgroundColor = Colors.white, }) : super(key: key); @override State<CustomTitleWidget> createState() => _CustomTitleWidgetState(); } class _CustomTitleWidgetState extends State<CustomTitleWidget> { final GlobalKey textKey = GlobalKey(); double textWidth = 0.0; double textHeight = 0.0; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final renderBox = textKey.currentContext?.findRenderObject() as RenderBox?; if (renderBox != null) { setState(() { textWidth = renderBox.size.width; textHeight = renderBox.size.height; }); } }); } @override Widget build(BuildContext context) { return Stack( clipBehavior: Clip.none, alignment: Alignment.topCenter, children: [ CustomPaint( painter: CustomDraw( widget.borderColor, textWidth, radius: widget.radius ?? 0, strokeWidth: widget.borderWidth, ), child: SizedBox( height: widget.height, width: widget.width, ), ), Positioned( top: -textHeight / 2, child: Container( key: textKey, color: widget.backgroundColor, padding: const EdgeInsets.symmetric(horizontal: 10), child: Text( widget.title, style: widget.titleStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), ), ], ); } }
修复说明
- 路径优化:用
arcToPoint替代cubicTo绘制圆角,同时考虑边框宽度偏移,避免圆角缺口 - 背景覆盖:将文字容器背景设为页面背景色,完全覆盖下方边框线,实现嵌入效果
- 参数扩展:增加边框颜色、宽度、背景色、文字样式等可配置项,提升组件复用性
- 性能优化:更新
shouldRepaint逻辑,仅在参数变化时重绘
内容的提问来源于stack exchange,提问作者Ömer Faruk Demirsoy
相关产品推荐
相关产品推荐

