In a custom Flutter RenderBox with children, how do parentData offsets, paint and hitTestChildren work together?
answer
- setupParentData installs the type
- performLayout writes the offset
- paintChild at offset plus child offset
- hit test in reverse paint order
- addWithPaintOffset subtracts it
basics
~20 sThe parent installs a parentData object on each child in setupParentData, writes each child's position into it during performLayout, paints each child at offset plus that position, and hit-tests children in reverse paint order after subtracting it.
solid answer
~40 s`parentData` is a slot on every child that belongs to its **parent's** layout algorithm. The parent's `setupParentData` installs the right type, for example a `ContainerBoxParentData` subclass that also links siblings. In `performLayout` the parent lays out each child and writes its position into `parentData.offset`. In `paint(context, offset)` it draws its own content, then calls `context.paintChild(child, offset + childParentData.offset)`. In `hitTestChildren` it walks children from last to first, the reverse of paint order so the topmost child wins, and calls `result.addWithPaintOffset(offset: childParentData.offset, position: position, hitTest: ...)`, which subtracts the offset and records the transform. `ContainerRenderObjectMixin` and `RenderBoxContainerDefaultsMixin` supply the child list plus `defaultPaint` and `defaultHitTestChildren` that do exactly this.
code
dart · 59 linesimport 'dart:math' as math;
import 'package:flutter/rendering.dart';
class RulerParentData extends ContainerBoxParentData<RenderBox> {}
/// Lays out one label child under every centimetre mark.
class RenderLabeledRuler extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, RulerParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, RulerParentData> {
RenderLabeledRuler({required double pixelsPerCm}) : _pixelsPerCm = pixelsPerCm;
static const double _tickBand = 24;
double _pixelsPerCm;
set pixelsPerCm(double value) {
if (value == _pixelsPerCm) return;
_pixelsPerCm = value;
markNeedsLayout();
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! RulerParentData) {
child.parentData = RulerParentData();
}
}
@override
void performLayout() {
var labelHeight = 0.0;
var index = 0;
RenderBox? child = firstChild;
while (child != null) {
child.layout(constraints.loosen(), parentUsesSize: true);
final RulerParentData data = child.parentData! as RulerParentData;
data.offset = Offset(index * _pixelsPerCm + 2, _tickBand);
labelHeight = math.max(labelHeight, child.size.height);
index += 1;
child = data.nextSibling;
}
size = constraints.constrain(Size(childCount * _pixelsPerCm, _tickBand + labelHeight));
}
@override
void paint(PaintingContext context, Offset offset) {
final Paint tick = Paint()..strokeWidth = 1;
for (var cm = 0; cm * _pixelsPerCm <= size.width; cm++) {
final double x = offset.dx + cm * _pixelsPerCm;
context.canvas.drawLine(Offset(x, offset.dy), Offset(x, offset.dy + _tickBand), tick);
}
defaultPaint(context, offset); // paints each child at offset + its parentData.offset
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) =>
defaultHitTestChildren(result, position: position);
}go deeper
Recall that a parent stores each child's position in the child's parentData and uses it when painting and hit-testing.
Explain setupParentData, writing offsets in performLayout, and paintChild at offset plus the child's offset.
Implement hitTestChildren in reverse paint order with addWithPaintOffset, and use the container mixins to avoid hand-written child bookkeeping.
Weigh a multi-child render object against composing existing layout widgets, considering hit testing, semantics and long-term ownership.
## One number, three readers When a render object has children, one piece of data ties its layout, painting and hit testing together: each child's **offset** relative to the parent. It lives in the child's **`parentData`**, a field every `RenderObject` has but whose content is owned by the parent's layout algorithm. Get it consistent and the three jobs line up; get one reader wrong and taps land on the wrong child or children paint in the wrong place. ## Step 1: install the parent data type When a child is adopted, the framework calls the parent's **`setupParentData(child)`**. `RenderBox`'s version installs a plain `BoxParentData`, which has an `offset`. A multi-child parent overrides it to install its own subclass, typically extending **`ContainerBoxParentData<RenderBox>`**, which adds `previousSibling` and `nextSibling` links used by **`ContainerRenderObjectMixin`** to keep the child list. Layout-specific fields go here too: `FlexParentData` carries `flex`, `StackParentData` carries `top`, `left` and friends. Widgets such as `Expanded` or `Positioned` write those fields through `ParentDataWidget`s. ## Step 2: write offsets in `performLayout` The woodworking app's labeled ruler has one label child per centimetre. `performLayout`: 1. walks the children from `firstChild` through each `parentData.nextSibling`; 2. lays out each with loose constraints and `parentUsesSize: true`; 3. sets `parentData.offset` to that centimetre's x position, below the tick band; 4. sets its own `size` from the number of centimetres and the tallest label. ## Step 3: paint at the combined offset `paint(PaintingContext context, Offset offset)` draws the ticks, then paints each child with **`context.paintChild(child, offset + childParentData.offset)`**. `paintChild` handles the case where a child is a repaint boundary with its own layer. The order of `paintChild` calls is the stacking order: later children paint on top. ## Step 4: hit-test with the same offsets `hitTestChildren(result, position: position)` receives the pointer position in the parent's local coordinates. For each child: - walk from `lastChild` backwards, so the child painted on top is tested first; - call **`result.addWithPaintOffset(offset: childParentData.offset, position: position, hitTest: (result, transformed) => child.hitTest(result, position: transformed))`**; - return `true` as soon as a child reports a hit. `addWithPaintOffset` subtracts the child's offset to get the child's local position and records the transform on the result, so pointer events later reach the child in its own coordinates. ## Tracing a tap on a label The user taps the "7 cm" label, 5 pixels right of its left edge: 1. The ruler's `hitTest` receives the position in the ruler's coordinates, checks it is inside `size`, and calls `hitTestChildren`. 2. `hitTestChildren` starts from the last child. For labels whose rectangle does not contain the point, the child's own `hitTest` returns `false`. 3. For the 7 cm label, `addWithPaintOffset` subtracts that label's `parentData.offset`, so the label sees a local position of about 5 pixels from its left edge, and its `hitTest` returns `true`. 4. The label and then the ruler are added to the result, and the search stops. The same offset that `performLayout` wrote was used by `paint` to draw the label there and by hit testing to find it there, so what the user sees is what receives the tap. ## The mixins that do it for you | Helper | Provides | |---|---| | `ContainerRenderObjectMixin<ChildType, ParentDataType>` | `firstChild`, `lastChild`, `childCount`, insert, move, remove, attach and detach | | `RenderBoxContainerDefaultsMixin` | `defaultPaint`, `defaultHitTestChildren`, and baseline helpers | | `RenderProxyBox` | A single child at offset zero, forwarding layout, paint and hit testing | | `RenderShiftedBox` | A single child at a `BoxParentData` offset | ## Bugs this prevents - **Painting children at `offset` only**: every label draws at the ruler's corner. - **Hit-testing without subtracting the offset**: the tap position is wrong for the child, so taps miss or hit the wrong label. - **Hit-testing in paint order**: where children overlap, the one underneath receives the tap. - **Forgetting `setupParentData`**: casting `child.parentData` to your subclass throws, because the child still has plain `BoxParentData`. How gesture widgets decide what to do with a hit, and how the custom widget that owns these children is declared, are separate topics.
- Why are children hit-tested from last to first when they are painted from first to last?Later children paint on top. Testing in reverse paint order means that where two children overlap, the one the user actually sees on top gets the first chance to claim the hit, and the search stops there.
- What goes wrong if a multi-child render object does not override setupParentData?Children keep the plain `BoxParentData` that `RenderBox` installs. Code that casts `child.parentData` to the container's own subclass throws a cast error, and the sibling links that `ContainerRenderObjectMixin` needs do not exist, so the child list cannot be maintained.
saying these in an interview costs you the question
- parentData belongs to the child's own layout algorithm.
- Children should be hit-tested in the same order they are painted.
- hitTestChildren receives global screen coordinates.
- paintChild should be called with the child's parentData offset alone.
- Every render object gets a ContainerBoxParentData automatically.