flutter添加间隙gap源码解析
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,
);
}
}
从源码看, 它提供了两个构造方法, Gap和Gap.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));
}
}
![]()
真正绘制原理, RenderGap是RenderBox的子类, 不需要子类, 绘制时, 只与自身size和color有关。
mainAxisExtent、crossAxisExtent属性set方法触发后, 会执行markNeedsLayout, 标记该渲染对象需要重新布局, 并请求(requestVisualUpdate)调度下一帧执行布局。
布局阶段,会执行computeDryLayout、performLayout方法,更新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)。