describeSemanticsClip method

  1. @override
Rect? describeSemanticsClip(
  1. covariant RenderObject child
)
override

Returns a rect in this object's coordinate system that describes which SemanticsNodes produced by the child should be included in the semantics tree. SemanticsNodes from the child that are positioned outside of this rect will be dropped. Child SemanticsNodes that are positioned inside this rect, but outside of describeApproximatePaintClip will be included in the tree marked as hidden. Child SemanticsNodes that are inside of both rect will be included in the tree as regular nodes.

This method only returns a non-null value if the semantics clip rect is different from the rect returned by describeApproximatePaintClip. If the semantics clip rect and the paint clip rect are the same, this method returns null.

A viewport would typically implement this method to include semantic nodes in the semantics tree that are currently hidden just before the leading or just after the trailing edge. These nodes have to be included in the semantics tree to implement implicit accessibility scrolling on iOS where the viewport scrolls implicitly when moving the accessibility focus from the last visible node in the viewport to the first hidden one.

See also:

Implementation

@override
Rect? describeSemanticsClip(RenderObject child) {
  // By default, Flutter derives the semantics clip from the paint clip. For
  // scrolling containers this causes offscreen descendants to get their
  // semantics rect clipped to empty, making them "invisible" and dropped
  // from the semantics tree. That breaks iOS VoiceOver focus geometry updates
  // when the focused node scrolls off-screen.
  //
  // Mirror RenderViewport.describeSemanticsClip by expanding the semantics
  // clip beyond the viewport so offscreen nodes remain in the tree and are
  // instead marked hidden via the paint clip.
  final CSSOverflowType overflowX = renderStyle.effectiveOverflowX;
  final CSSOverflowType overflowY = renderStyle.effectiveOverflowY;

  final Size? viewport = _viewportSize;
  final Size? content = _scrollableSize;

  final bool xScrollable = (overflowX == CSSOverflowType.scroll || overflowX == CSSOverflowType.auto) &&
      scrollOffsetX != null &&
      viewport != null &&
      content != null;
  final bool yScrollable = (overflowY == CSSOverflowType.scroll || overflowY == CSSOverflowType.auto) &&
      scrollOffsetY != null &&
      viewport != null &&
      content != null;

  if (!xScrollable && !yScrollable) {
    return super.describeSemanticsClip(child);
  }

  final double maxX = xScrollable ? math.max(0.0, content!.width - viewport!.width) : 0.0;
  final double maxY = yScrollable ? math.max(0.0, content!.height - viewport!.height) : 0.0;

  final Rect bounds = semanticBounds;
  if (maxX == 0.0 && maxY == 0.0) {
    return bounds;
  }

  // Match Flutter's _RenderSingleChildViewport.describeSemanticsClip:
  // expand the semantics clip by the already scrolled distance and the
  // remaining scrollable distance, so offscreen descendants remain in the
  // semantics tree (and become hidden via paint clip instead of dropped).
  double left = bounds.left;
  double right = bounds.right;
  double top = bounds.top;
  double bottom = bounds.bottom;

  if (xScrollable) {
    final double posX = scrollLeft.clamp(0.0, maxX).toDouble();
    final double remainingX = maxX - posX;
    if (renderStyle.direction == TextDirection.rtl) {
      // Equivalent to AxisDirection.left.
      left -= remainingX;
      right += posX;
    } else {
      // AxisDirection.right.
      left -= posX;
      right += remainingX;
    }
  }

  if (yScrollable) {
    final double posY = scrollTop.clamp(0.0, maxY).toDouble();
    final double remainingY = maxY - posY;
    // AxisDirection.down.
    top -= posY;
    bottom += remainingY;
  }

  return Rect.fromLTRB(left, top, right, bottom);
}