Skip to content
This repository was archived by the owner on Oct 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Delay visibility determination
  • Loading branch information
dnfield committed May 11, 2022
commit a81e31178d9c66aa3d138b34683efdfae300499c
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ mixin RenderVisibilityDetectorBase on RenderObject {
return _updates.length;
}

static Map<Key, RenderVisibilityDetectorBase> _updates =
<Key, RenderVisibilityDetectorBase>{};
static Map<Key, VoidCallback> _updates = <Key, VoidCallback>{};
static Map<Key, VisibilityInfo> _lastVisibility = <Key, VisibilityInfo>{};

/// See [VisibilityDetectorController.notifyNow].
Expand Down Expand Up @@ -58,17 +57,15 @@ mixin RenderVisibilityDetectorBase on RenderObject {

/// Executes visibility callbacks for all updated instances.
static void _processCallbacks() {
for (final detector in _updates.values) {
detector._fireCallback();
for (final callback in _updates.values) {
callback();
}
_updates.clear();
}

void _fireCallback() {
assert(_info != null);

void _fireCallback(ContainerLayer layer, Rect bounds) {
final oldInfo = _lastVisibility[key];
final info = _info!;
final info = _determineVisibility(layer, bounds);
final visible = !info.visibleBounds.isEmpty;

if (oldInfo == null) {
Expand Down Expand Up @@ -111,23 +108,17 @@ mixin RenderVisibilityDetectorBase on RenderObject {
forget(key);
_compositionCallbackCanceller?.call();
_compositionCallbackCanceller = null;
_info = null;
_lastVisibility.remove(key);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering, why does forget not also clear the lastVisibility for the key? Is there a use case where you want to forget, but still keep that historical information?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this to forget

} else {
markNeedsPaint();
}
}

VisibilityInfo? _info;

/// Invokes the visibility callback if [VisibilityInfo] hasn't meaningfully
/// changed since the last time we invoked it.
void _setInfo(VisibilityInfo value) {
if (_info != null && value.matchesVisibility(_info!)) {
return;
}
void _scheduleUpdate(ContainerLayer layer, Rect bounds) {
bool isFirstUpdate = _updates.isEmpty;
_updates[key] = this;
_updates[key] = () {
_fireCallback(layer, bounds);
};
final updateInterval = VisibilityDetectorController.instance.updateInterval;
if (updateInterval == Duration.zero) {
// Even with [Duration.zero], we still want to defer callbacks to the end
Expand All @@ -148,14 +139,15 @@ mixin RenderVisibilityDetectorBase on RenderObject {
} else {
assert(_timer!.isActive);
}
_info = value;
}

void _determineVisibility(ContainerLayer layer, Rect bounds) {
if (!layer.attached) {
VisibilityInfo _determineVisibility(ContainerLayer layer, Rect bounds) {
if (_disposed || !layer.attached || !attached) {
// layer is detached and thus invisible.
_setInfo(VisibilityInfo(key: key, size: _info?.size ?? Size.zero));
return;
return VisibilityInfo(
key: key,
size: _lastVisibility[key]?.size ?? Size.zero,
);
}
final transform = Matrix4.identity();

Expand Down Expand Up @@ -186,22 +178,19 @@ mixin RenderVisibilityDetectorBase on RenderObject {
clip = clip.intersect(MatrixUtils.transformRect(transform, parentClip));
}
}
_setInfo(VisibilityInfo.fromRects(
return VisibilityInfo.fromRects(
key: key,
widgetBounds: MatrixUtils.transformRect(transform, bounds),
clipRect: clip,
));
);
}

// Needed in case we get called back after disposal.
bool _disposed = false;

@override
void dispose() {
_setInfo(VisibilityInfo(key: key, size: _info?.size ?? Size.zero));
_disposed = true;
_compositionCallbackCanceller?.call();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to "forget" this RO in dispose so the visibility logic is no longer triggered for it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, never mind. You do want them to fire one more time after it got disposed it seems... Odd.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that may be our only chance to let clients know that the RO is no longer visible (assuming it ever was). There are tests that fail if you add a forget here.

_compositionCallbackCanceller = null;
_disposed = true;
super.dispose();
}
}
Expand All @@ -228,8 +217,8 @@ class RenderVisibilityDetector extends RenderProxyBox
if (onVisibilityChanged != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like paint here and in RenderSliverVisibilityDetector have a lot in common, the inly difference is the second argument passed to _scheduleUpdate? Maybe move paint to the base and have this class and RenderSliverVisibilityDetector implement just a method that returns the value for that second argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, as mentioned elsehwere moved this to a Rect get bounds; on the mixin, which simplified things a bit.

_compositionCallbackCanceller =
context.addCompositionCallback((ContainerLayer layer) {
assert(!_disposed);
_determineVisibility(layer, offset & semanticBounds.size);
assert(!debugDisposed!);
_scheduleUpdate(layer, offset & semanticBounds.size);
});
}
super.paint(context, offset);
Expand Down Expand Up @@ -259,7 +248,7 @@ class RenderSliverVisibilityDetector extends RenderProxySliver
void paint(PaintingContext context, Offset offset) {
if (onVisibilityChanged != null) {
context.addCompositionCallback((ContainerLayer layer) {
assert(!_disposed);
assert(!debugDisposed!);

Size widgetSize;
Offset widgetOffset;
Expand Down Expand Up @@ -294,7 +283,7 @@ class RenderSliverVisibilityDetector extends RenderProxySliver
Size(geometry!.scrollExtent, constraints.crossAxisExtent);
break;
}
_determineVisibility(layer, offset + widgetOffset & widgetSize);
_scheduleUpdate(layer, offset + widgetOffset & widgetSize);
});
}
super.paint(context, offset);
Expand Down
7 changes: 4 additions & 3 deletions packages/visibility_detector/lib/src/visibility_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,16 @@ class VisibilityInfo {
assert(widgetBounds != null);
assert(clipRect != null);

final bool overlaps = widgetBounds.overlaps(clipRect);
// Compute the intersection in the widget's local coordinates.
final visibleBounds = widgetBounds.overlaps(clipRect)
final visibleBounds = overlaps
? widgetBounds.intersect(clipRect).shift(-widgetBounds.topLeft)
: Rect.zero;

return VisibilityInfo(
key: key,
size: widgetBounds.size,
screenRect: widgetBounds,
screenRect: overlaps ? widgetBounds : Rect.zero,
visibleBounds: visibleBounds);
}

Expand Down Expand Up @@ -196,7 +197,7 @@ class VisibilityInfo {

@override
String toString() {
return 'VisibilityInfo(size: $size visibleBounds: $visibleBounds, screenRect: $screenRect)';
return 'VisibilityInfo(key: $key, size: $size visibleBounds: $visibleBounds, screenRect: $screenRect)';
}
}

Expand Down