getOffsetToReveal method Null safety
- RenderObject target,
- double alignment,
- {Rect? rect}
Returns the offset that would be needed to reveal the target
RenderObject.
This is used by RenderViewportBase.showInViewport, which is itself used by RenderObject.showOnScreen for RenderViewportBase, which is in turn used by the semantics system to implement scrolling for accessibility tools.
The optional rect
parameter describes which area of that target
object
should be revealed in the viewport. If rect
is null, the entire
target
RenderObject (as defined by its RenderObject.paintBounds)
will be revealed. If rect
is provided it has to be given in the
coordinate system of the target
object.
The alignment
argument describes where the target should be positioned
after applying the returned offset. If alignment
is 0.0, the child must
be positioned as close to the leading edge of the viewport as possible. If
alignment
is 1.0, the child must be positioned as close to the trailing
edge of the viewport as possible. If alignment
is 0.5, the child must be
positioned as close to the center of the viewport as possible.
The target
might not be a direct child of this viewport but it must be a
descendant of the viewport. Other viewports in between this viewport and
the target
will not be adjusted.
This method assumes that the content of the viewport moves linearly, i.e.
when the offset of the viewport is changed by x then target
also moves
by x within the viewport.
See also:
- RevealedOffset, which describes the return value of this method.
Implementation
@override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) {
// Steps to convert `rect` (from a RenderBox coordinate system) to its
// scroll offset within this viewport (not in the exact order):
//
// 1. Pick the outermost RenderBox (between which, and the viewport, there
// is nothing but RenderSlivers) as an intermediate reference frame
// (the `pivot`), convert `rect` to that coordinate space.
//
// 2. Convert `rect` from the `pivot` coordinate space to its sliver
// parent's sliver coordinate system (i.e., to a scroll offset), based on
// the axis direction and growth direction of the parent.
//
// 3. Convert the scroll offset to its sliver parent's coordinate space
// using `childScrollOffset`, until we reach the viewport.
//
// 4. Make the final conversion from the outmost sliver to the viewport
// using `scrollOffsetOf`.
double leadingScrollOffset = 0.0;
// Starting at `target` and walking towards the root:
// - `child` will be the last object before we reach this viewport, and
// - `pivot` will be the last RenderBox before we reach this viewport.
RenderObject child = target;
RenderBox? pivot;
bool onlySlivers = target is RenderSliver; // ... between viewport and `target` (`target` included).
while (child.parent != this) {
final RenderObject parent = child.parent! as RenderObject;
assert(parent != null, '$target must be a descendant of $this');
if (child is RenderBox) {
pivot = child;
}
if (parent is RenderSliver) {
leadingScrollOffset += parent.childScrollOffset(child)!;
} else {
onlySlivers = false;
leadingScrollOffset = 0.0;
}
child = parent;
}
// `rect` in the new intermediate coordinate system.
final Rect rectLocal;
// Our new reference frame render object's main axis extent.
final double pivotExtent;
final GrowthDirection growthDirection;
// `leadingScrollOffset` is currently the scrollOffset of our new reference
// frame (`pivot` or `target`), within `child`.
if (pivot != null) {
assert(pivot.parent != null);
assert(pivot.parent != this);
assert(pivot != this);
assert(pivot.parent is RenderSliver); // TODO(abarth): Support other kinds of render objects besides slivers.
final RenderSliver pivotParent = pivot.parent! as RenderSliver;
growthDirection = pivotParent.constraints.growthDirection;
switch (axis) {
case Axis.horizontal:
pivotExtent = pivot.size.width;
break;
case Axis.vertical:
pivotExtent = pivot.size.height;
break;
}
rect ??= target.paintBounds;
rectLocal = MatrixUtils.transformRect(target.getTransformTo(pivot), rect);
} else if (onlySlivers) {
// `pivot` does not exist. We'll have to make up one from `target`, the
// innermost sliver.
final RenderSliver targetSliver = target as RenderSliver;
growthDirection = targetSliver.constraints.growthDirection;
// TODO(LongCatIsLooong): make sure this works if `targetSliver` is a
// persistent header, when #56413 relands.
pivotExtent = targetSliver.geometry!.scrollExtent;
if (rect == null) {
switch (axis) {
case Axis.horizontal:
rect = Rect.fromLTWH(
0, 0,
targetSliver.geometry!.scrollExtent,
targetSliver.constraints.crossAxisExtent,
);
break;
case Axis.vertical:
rect = Rect.fromLTWH(
0, 0,
targetSliver.constraints.crossAxisExtent,
targetSliver.geometry!.scrollExtent,
);
break;
}
}
rectLocal = rect;
} else {
assert(rect != null);
return RevealedOffset(offset: offset.pixels, rect: rect!);
}
assert(pivotExtent != null);
assert(rect != null);
assert(rectLocal != null);
assert(growthDirection != null);
assert(child.parent == this);
assert(child is RenderSliver);
final RenderSliver sliver = child as RenderSliver;
final double targetMainAxisExtent;
// The scroll offset of `rect` within `child`.
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
case AxisDirection.up:
leadingScrollOffset += pivotExtent - rectLocal.bottom;
targetMainAxisExtent = rectLocal.height;
break;
case AxisDirection.right:
leadingScrollOffset += rectLocal.left;
targetMainAxisExtent = rectLocal.width;
break;
case AxisDirection.down:
leadingScrollOffset += rectLocal.top;
targetMainAxisExtent = rectLocal.height;
break;
case AxisDirection.left:
leadingScrollOffset += pivotExtent - rectLocal.right;
targetMainAxisExtent = rectLocal.width;
break;
}
// So far leadingScrollOffset is the scroll offset of `rect` in the `child`
// sliver's sliver coordinate system. The sign of this value indicates
// whether the `rect` protrudes the leading edge of the `child` sliver. When
// this value is non-negative and `child`'s `maxScrollObstructionExtent` is
// greater than 0, we assume `rect` can't be obstructed by the leading edge
// of the viewport (i.e. its pinned to the leading edge).
final bool isPinned = sliver.geometry!.maxScrollObstructionExtent > 0 && leadingScrollOffset >= 0;
// The scroll offset in the viewport to `rect`.
leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
// This step assumes the viewport's layout is up-to-date, i.e., if
// offset.pixels is changed after the last performLayout, the new scroll
// position will not be accounted for.
final Matrix4 transform = target.getTransformTo(this);
Rect targetRect = MatrixUtils.transformRect(transform, rect);
final double extentOfPinnedSlivers = maxScrollObstructionExtentBefore(sliver);
switch (sliver.constraints.growthDirection) {
case GrowthDirection.forward:
if (isPinned && alignment <= 0) {
return RevealedOffset(offset: double.infinity, rect: targetRect);
}
leadingScrollOffset -= extentOfPinnedSlivers;
break;
case GrowthDirection.reverse:
if (isPinned && alignment >= 1) {
return RevealedOffset(offset: double.negativeInfinity, rect: targetRect);
}
// If child's growth direction is reverse, when viewport.offset is
// `leadingScrollOffset`, it is positioned just outside of the leading
// edge of the viewport.
switch (axis) {
case Axis.vertical:
leadingScrollOffset -= targetRect.height;
break;
case Axis.horizontal:
leadingScrollOffset -= targetRect.width;
break;
}
break;
}
final double mainAxisExtent;
switch (axis) {
case Axis.horizontal:
mainAxisExtent = size.width - extentOfPinnedSlivers;
break;
case Axis.vertical:
mainAxisExtent = size.height - extentOfPinnedSlivers;
break;
}
final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
final double offsetDifference = offset.pixels - targetOffset;
switch (axisDirection) {
case AxisDirection.down:
targetRect = targetRect.translate(0.0, offsetDifference);
break;
case AxisDirection.right:
targetRect = targetRect.translate(offsetDifference, 0.0);
break;
case AxisDirection.up:
targetRect = targetRect.translate(0.0, -offsetDifference);
break;
case AxisDirection.left:
targetRect = targetRect.translate(-offsetDifference, 0.0);
break;
}
return RevealedOffset(offset: targetOffset, rect: targetRect);
}