普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月30日首页

flutter添加间隙gap源码解析

作者 Nicholas68
2026年1月30日 20:46

Flutter 小部件,可轻松在 Flex 小部件(如列和行)或滚动视图中添加间隙。

Gap的核心原理是使用RenderObject自定义实现布局。

Gap


class Gap extends StatelessWidget {
  const Gap(
    this.mainAxisExtent, {
    Key? key,
    this.crossAxisExtent,
    this.color,
  })  : assert(mainAxisExtent >= 0 && mainAxisExtent < double.infinity),
        assert(crossAxisExtent == null || crossAxisExtent >= 0),
        super(key: key);

  const Gap.expand(
    double mainAxisExtent, {
    Key? key,
    Color? color,
  }) : this(
          mainAxisExtent,
          key: key,
          crossAxisExtent: double.infinity,
          color: color,
        );

  final double mainAxisExtent;

  final double? crossAxisExtent;

  final Color? color;

  @override
  Widget build(BuildContext context) {
    final scrollableState = Scrollable.maybeOf(context);
    final AxisDirection? axisDirection = scrollableState?.axisDirection;
    final Axis? fallbackDirection =
        axisDirection == null ? null : axisDirectionToAxis(axisDirection);

    return _RawGap(
      mainAxisExtent,
      crossAxisExtent: crossAxisExtent,
      color: color,
      fallbackDirection: fallbackDirection,
    );
  }
}

从源码看, 它提供了两个构造方法, GapGap.expand方便用户按需使用。

_RawGap

_RawGap是核心类, 它继承了LeafRenderObjectWidget.

class _RawGap extends LeafRenderObjectWidget {
  const _RawGap(
    this.mainAxisExtent, {
    Key? key,
    this.crossAxisExtent,
    this.color,
    this.fallbackDirection,
  })  : assert(mainAxisExtent >= 0 && mainAxisExtent < double.infinity),
        assert(crossAxisExtent == null || crossAxisExtent >= 0),
        super(key: key);

  final double mainAxisExtent;

  final double? crossAxisExtent;

  final Color? color;

  final Axis? fallbackDirection;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderGap(
      mainAxisExtent: mainAxisExtent,
      crossAxisExtent: crossAxisExtent ?? 0,
      color: color,
      fallbackDirection: fallbackDirection,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderGap renderObject) {
    if (kDebugMode) {
      debugPrint(
        '[Gap] updateRenderObject '
        'mainAxisExtent=$mainAxisExtent '
        'crossAxisExtent=${crossAxisExtent ?? 0} '
        'color=$color '
        'fallbackDirection=$fallbackDirection',
      );
    }
    renderObject
      ..mainAxisExtent = mainAxisExtent
      ..crossAxisExtent = crossAxisExtent ?? 0
      ..color = color
      ..fallbackDirection = fallbackDirection;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty('mainAxisExtent', mainAxisExtent));
    properties.add(
        DoubleProperty('crossAxisExtent', crossAxisExtent, defaultValue: 0));
    properties.add(ColorProperty('color', color));
    properties.add(EnumProperty<Axis>('fallbackDirection', fallbackDirection));
  }
}

RenderGap

class RenderGap extends RenderBox {
  RenderGap({
    required double mainAxisExtent,
    double? crossAxisExtent,
    Axis? fallbackDirection,
    Color? color,
  })  : _mainAxisExtent = mainAxisExtent,
        _crossAxisExtent = crossAxisExtent,
        _color = color,
        _fallbackDirection = fallbackDirection {
    if (kDebugMode) {
      debugPrint(
        '🆕 RenderGap<init> '
        'mainAxisExtent=$mainAxisExtent '
        'crossAxisExtent=$crossAxisExtent '
        'color=$color '
        'fallbackDirection=$fallbackDirection',
      );
    }
  }

  double get mainAxisExtent => _mainAxisExtent;
  double _mainAxisExtent;
  set mainAxisExtent(double value) {
    if (_mainAxisExtent != value) {
      if (kDebugMode) {
        debugPrint('📏 mainAxisExtent set: $_mainAxisExtent -> $value');
      }
      _mainAxisExtent = value;
      markNeedsLayout();
    }
  }

  double? get crossAxisExtent => _crossAxisExtent;
  double? _crossAxisExtent;
  set crossAxisExtent(double? value) {
    if (_crossAxisExtent != value) {
      if (kDebugMode) {
        debugPrint('📐 crossAxisExtent set: $_crossAxisExtent -> $value');
      }
      _crossAxisExtent = value;
      markNeedsLayout();
    }
  }

  Axis? get fallbackDirection => _fallbackDirection;
  Axis? _fallbackDirection;
  set fallbackDirection(Axis? value) {
    if (_fallbackDirection != value) {
      if (kDebugMode) {
        debugPrint('🧭 fallbackDirection set: $_fallbackDirection -> $value');
      }
      _fallbackDirection = value;
      markNeedsLayout();
    }
  }

  Axis? get _direction {
    final parentNode = parent;
    if (parentNode is RenderFlex) {
      return parentNode.direction;
    } else {
      return fallbackDirection;
    }
  }

  Color? get color => _color;
  Color? _color;
  set color(Color? value) {
    if (_color != value) {
      if (kDebugMode) {
        debugPrint('🎨 color set: $_color -> $value');
      }
      _color = value;
      markNeedsPaint();
    }
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    final result = _computeIntrinsicExtent(
      Axis.horizontal,
      () => super.computeMinIntrinsicWidth(height),
    )!;
    if (kDebugMode) {
      debugPrint('🔹 computeMinIntrinsicWidth(height=$height) => $result');
    }
    return result;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    final result = _computeIntrinsicExtent(
      Axis.horizontal,
      () => super.computeMaxIntrinsicWidth(height),
    )!;
    if (kDebugMode) {
      debugPrint('🔷 computeMaxIntrinsicWidth(height=$height) => $result');
    }
    return result;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    final result = _computeIntrinsicExtent(
      Axis.vertical,
      () => super.computeMinIntrinsicHeight(width),
    )!;
    if (kDebugMode) {
      debugPrint('🔸 computeMinIntrinsicHeight(width=$width) => $result');
    }
    return result;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    final result = _computeIntrinsicExtent(
      Axis.vertical,
      () => super.computeMaxIntrinsicHeight(width),
    )!;
    if (kDebugMode) {
      debugPrint('🔶 computeMaxIntrinsicHeight(width=$width) => $result');
    }
    return result;
  }

  double? _computeIntrinsicExtent(Axis axis, double Function() compute) {
    final Axis? direction = _direction;
    if (direction == axis) {
      final result = _mainAxisExtent;
      if (kDebugMode) {
        debugPrint(
          '📐 _computeIntrinsicExtent(axis=$axis, direction=$direction) => $result',
        );
      }
      return result;
    } else {
      if (_crossAxisExtent!.isFinite) {
        final result = _crossAxisExtent;
        if (kDebugMode) {
          debugPrint(
            '📐 _computeIntrinsicExtent(axis=$axis, direction=$direction) => $result',
          );
        }
        return result;
      } else {
        final result = compute();
        if (kDebugMode) {
          debugPrint(
            '📐 _computeIntrinsicExtent(axis=$axis, direction=$direction) => $result',
          );
        }
        return result;
      }
    }
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    final Axis? direction = _direction;

    if (direction != null) {
      if (direction == Axis.horizontal) {
        final s =
            constraints.constrain(Size(mainAxisExtent, crossAxisExtent!));
        if (kDebugMode) {
          debugPrint(
            '💧 computeDryLayout(constraints=$constraints, direction=$direction) => $s',
          );
        }
        return s;
      } else {
        final s =
            constraints.constrain(Size(crossAxisExtent!, mainAxisExtent));
        if (kDebugMode) {
          debugPrint(
            '💧 computeDryLayout(constraints=$constraints, direction=$direction) => $s',
          );
        }
        return s;
      }
    } else {
      throw FlutterError(
        'A Gap widget must be placed directly inside a Flex widget '
        'or its fallbackDirection must not be null',
      );
    }
  }

  @override
  void performLayout() {
    size = computeDryLayout(constraints);
    if (kDebugMode) {
      debugPrint('🛠 performLayout(constraints=$constraints) size=$size');
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (color != null) {
      final Paint paint = Paint()..color = color!;
      context.canvas.drawRect(offset & size, paint);
      if (kDebugMode) {
        debugPrint('🎨 paint(offset=$offset, size=$size, color=$color)');
      }
    } else {
      if (kDebugMode) {
        debugPrint('🎨 paint(offset=$offset, size=$size, color=null)');
      }
    }
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    if (kDebugMode) {
      debugPrint('🧾 debugFillProperties()');
    }
    properties.add(DoubleProperty('mainAxisExtent', mainAxisExtent));
    properties.add(DoubleProperty('crossAxisExtent', crossAxisExtent));
    properties.add(ColorProperty('color', color));
    properties.add(EnumProperty<Axis>('fallbackDirection', fallbackDirection));
  }
}

截屏2026-01-30 19.30.39.png

真正绘制原理, RenderGapRenderBox的子类, 不需要子类, 绘制时, 只与自身sizecolor有关。

mainAxisExtentcrossAxisExtent属性set方法触发后, 会执行markNeedsLayout, 标记该渲染对象需要重新布局, 并请求(requestVisualUpdate)调度下一帧执行布局。

布局阶段,会执行computeDryLayoutperformLayout方法,更新size

绘制阶段paint,在 offset & size 的矩形内填充颜色(color 为 null 时不绘制)。

  • 矩形范围:offset & size 等价于 Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),保证绘制严格位于本组件区域。
  • 无子节点与图层:RenderGap 不 push 额外 Layer,也不绘制子内容;仅把一个矩形指令提交到当前画布。

markNeedsLayout布局阶段不同的是, markNeedsPaint绘制阶段不参与尺寸计算, 它在size确定后才执行。

与标记方法的关系:

  • markNeedsPaint:当 color 变更时由属性 setter 调用,标记本节点需要在下一帧重绘;不会触发布局。
  • markNeedsLayout:当 mainAxisExtent/crossAxisExtent/fallbackDirection 变更引起尺寸或方向变化时调用;下一帧会重新布局,布局完成后若绘制区域或内容也需更新才会出现 paint。
  • 执行链路示例:属性变更 → 标记(layout/paint)→ 布局(computeDryLayout/performLayout)→ 绘制(paint)。
❌
❌