performLayout method

  1. @override
void performLayout()
override

Do the work of computing the layout for this render object.

Do not call this function directly: call layout instead. This function is called by layout when there is actually work to be done by this render object during layout. The layout constraints provided by your parent are available via the constraints getter.

If sizedByParent is true, then this function should not actually change the dimensions of this render object. Instead, that work should be done by performResize. If sizedByParent is false, then this function should both change the dimensions of this render object and instruct its children to layout.

In implementing this function, you must call layout on each of your children, passing true for parentUsesSize if your layout information is dependent on your child's layout information. Passing true for parentUsesSize ensures that this render object will undergo layout if the child undergoes layout. Otherwise, the child can change its layout information without informing this render object.

Some special RenderObject subclasses (such as the one used by OverlayPortal.overlayChildLayoutBuilder) call applyPaintTransform in their performLayout implementation. To ensure such RenderObjects get the up-to-date paint transform, RenderObject subclasses should typically update the paint transform (as reported by applyPaintTransform) in this method instead of paint.

Implementation

@override
void performLayout() {
  size = constraints.biggest;

  if (childCount == 0) {
    return;
  }

  final double layoutCenterX = size.width / 2.0;
  final double layoutCenterY = size.height / 2.0;

  // Pass 1: Layout children, extract their radii, and set deterministic starting positions.
  int index = 0;
  RenderBox? child = firstChild;
  while (child != null) {
    final BubbleCloudParentData childParentData = child.parentData! as BubbleCloudParentData;

    // We allow children to determine their own intrinsic size (unbounded).
    child.layout(const BoxConstraints(), parentUsesSize: true);

    // Assume the child is roughly circular. The radius is half the largest dimension.
    childParentData.radius = math.max(child.size.width, child.size.height) / 2.0;

    // To ensure the physics layout is perfectly deterministic across rebuilds,
    // we initialize them in a small, tight circle around the center based on their index,
    // rather than using random coordinates.
    final double angle = index * (math.pi * 2 / childCount);
    childParentData.centerX = layoutCenterX + math.cos(angle) * 10;
    childParentData.centerY = layoutCenterY + math.sin(angle) * 10;

    child = childParentData.nextSibling;
    index++;
  }

  // Pass 2: The Force-Directed Physics Loop (Position-Based Dynamics)
  for (int step = 0; step < _simulationSteps; step++) {
    // Force A: Repulsion (Collision Resolution)
    RenderBox? c1 = firstChild;
    while (c1 != null) {
      final BubbleCloudParentData pd1 = c1.parentData! as BubbleCloudParentData;

      RenderBox? c2 = pd1.nextSibling;
      while (c2 != null) {
        final BubbleCloudParentData pd2 = c2.parentData! as BubbleCloudParentData;

        final double dx = pd2.centerX - pd1.centerX;
        final double dy = pd2.centerY - pd1.centerY;
        final double distance = math.sqrt(dx * dx + dy * dy);
        final double minAllowedDistance = pd1.radius + pd2.radius;

        // If overlapping, push them apart along the vector between their centers.
        if (distance < minAllowedDistance && distance > 0) {
          final double overlap = minAllowedDistance - distance;

          // Normalize the vector
          final double nx = dx / distance;
          final double ny = dy / distance;

          // Split the displacement evenly between the two bubbles
          final double displaceX = nx * overlap * _repulsionDamping;
          final double displaceY = ny * overlap * _repulsionDamping;

          pd1.centerX -= displaceX;
          pd1.centerY -= displaceY;
          pd2.centerX += displaceX;
          pd2.centerY += displaceY;
        }
        c2 = pd2.nextSibling;
      }
      c1 = pd1.nextSibling;
    }

    // Force B: Attraction (Pull toward center)
    c1 = firstChild;
    while (c1 != null) {
      final BubbleCloudParentData pd = c1.parentData! as BubbleCloudParentData;
      pd.centerX += (layoutCenterX - pd.centerX) * _attractionForce;
      pd.centerY += (layoutCenterY - pd.centerY) * _attractionForce;
      c1 = pd.nextSibling;
    }
  }

  // Pass 3: Convert the final physical center coordinates back into top-left UI offsets.
  child = firstChild;
  while (child != null) {
    final BubbleCloudParentData childParentData = child.parentData! as BubbleCloudParentData;

    // Constrain bubbles to stay within the layout box
    final double clampedX = childParentData.centerX.clamp(
      childParentData.radius,
      size.width - childParentData.radius,
    );
    final double clampedY = childParentData.centerY.clamp(
      childParentData.radius,
      size.height - childParentData.radius,
    );

    childParentData.offset = Offset(clampedX - (child.size.width / 2.0), clampedY - (child.size.height / 2.0));

    child = childParentData.nextSibling;
  }
}