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

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

效果示意图
问题示意图

实现方案

这里提供两种可直接落地的方案,都支持完全复用原生Container的decoration属性,渐变自动同步到三角形。

方案1:统一渐变作用域(推荐,性能最优)

核心思路是把三角形和Container看作一个整体,统一设置渐变,不需要分别为两个元素配置渐变:

  1. 自定义组件继承StatelessWidget,透传所有Container支持的属性,额外新增indicatorWidth、indicatorHeight、indicatorLeftOffset三个参数控制三角形的尺寸和位置。
  2. 用Stack层叠结构:底层用CustomPaint绘制三角形,上层放置带内容的Container,顶部预留三角形高度的边距。
  3. 从传入的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.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 17:24:07