Flutter如何实现带可定位渐变三角形指示器的自定义Container组件


实现方案
这里提供两种可直接落地的方案,都支持完全复用原生Container的decoration属性,渐变自动同步到三角形。
方案1:统一渐变作用域(推荐,性能最优)
核心思路是把三角形和Container看作一个整体,统一设置渐变,不需要分别为两个元素配置渐变:
- 自定义组件继承
StatelessWidget,透传所有Container支持的属性,额外新增indicatorWidth、indicatorHeight、indicatorLeftOffset三个参数控制三角形的尺寸和位置。 - 用
Stack层叠结构:底层用CustomPaint绘制三角形,上层放置带内容的Container,顶部预留三角形高度的边距。 - 从传入的
decoration中提取Gradient属性,将渐变的作用范围设置为整个自定义组件的尺寸,三角形和Container的绘制都复用这个渐变配置。
可直接复制使用的示例代码:
class IndicatorContainer extends StatelessWidget { final double indicatorWidth; final double indicatorHeight; final double indicatorLeftOffset; final Decoration? decoration; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; final Widget? child; const IndicatorContainer({ super.key, required this.indicatorWidth, required this.indicatorHeight, required this.indicatorLeftOffset, this.decoration, this.padding, this.margin, this.child, }); @override Widget build(BuildContext context) { return Container( margin: margin, child: Stack( clipBehavior: Clip.none, children: [ // 绘制三角形 Positioned( left: indicatorLeftOffset, top: -indicatorHeight, child: CustomPaint( painter: _TrianglePainter( indicatorHeight: indicatorHeight, indicatorWidth: indicatorWidth, decoration: decoration, ), size: Size(indicatorWidth, indicatorHeight), ), ), // 主体Container,透传所有配置 Container( decoration: decoration, padding: padding, child: child, ), ], ), ); } } class _TrianglePainter extends CustomPainter { final double indicatorWidth; final double indicatorHeight; final Decoration? decoration; _TrianglePainter({ required this.indicatorWidth, required this.indicatorHeight, required this.decoration, }); @override void paint(Canvas canvas, Size size) { final paint = Paint(); // 复用Container的渐变配置 if (decoration is BoxDecoration) { final boxDeco = decoration as BoxDecoration; if (boxDeco.gradient != null) { // 渐变作用范围覆盖三角形+整个Container区域 final parentRenderBox = super.findRenderObject()?.parent as RenderBox?; final totalHeight = indicatorHeight + (parentRenderBox?.size.height ?? 0); paint.shader = boxDeco.gradient!.createShader( Rect.fromLTWH(-indicatorLeftOffset, -indicatorHeight, parentRenderBox?.size.width ?? size.width, totalHeight), ); } else if (boxDeco.color != null) { paint.color = boxDeco.color!; } } // 绘制三角形路径 final path = Path() ..moveTo(0, indicatorHeight) ..lineTo(indicatorWidth / 2, 0) ..lineTo(indicatorWidth, indicatorHeight) ..close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant _TrianglePainter oldDelegate) { return oldDelegate.decoration != decoration || oldDelegate.indicatorWidth != indicatorWidth || oldDelegate.indicatorHeight != indicatorHeight; } }
使用方式和原生Container完全一致:
IndicatorContainer( indicatorWidth: 20, indicatorHeight: 10, indicatorLeftOffset: 30, decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue, Colors.green], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.all(16), child: const Text("测试内容"), )
方案2:用ShaderMask统一着色
如果不想自定义Painter,也可以用ShaderMask包裹整个组合组件,把渐变作为着色器统一应用到所有子元素:
ShaderMask( shaderCallback: (bounds) => const LinearGradient( colors: [Colors.blue, Colors.green], begin: Alignment.topCenter, end: Alignment.bottomCenter, ).createShader(bounds), blendMode: BlendMode.srcIn, child: Stack( clipBehavior: Clip.none, children: [ // 任意三角形实现都可以,不需要单独设置颜色 Positioned( left: 30, top: -10, child: CustomPaint( painter: _SimpleTrianglePainter(), size: const Size(20,10), ) ), Container( margin: const EdgeInsets.only(top: 10), color: Colors.white, // 颜色会被着色器覆盖,可任意填写 padding: const EdgeInsets.all(16), child: const Text("测试内容"), ), ], ), ) class _SimpleTrianglePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final path = Path() ..moveTo(0, size.height) ..lineTo(size.width/2, 0) ..lineTo(size.width, size.height) ..close(); canvas.drawPath(path, Paint()..color = Colors.white); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }
这种方式的优势是代码更简洁,劣势是如果组件内部有其他不需要渐变的元素,需要单独包裹排除着色。
内容的提问来源于stack exchange,提问作者Tobi B.
相关产品推荐
相关产品推荐

