diff --git a/packages/visibility_detector/CHANGELOG.md b/packages/visibility_detector/CHANGELOG.md index 2f6b516c..efc2038e 100644 --- a/packages/visibility_detector/CHANGELOG.md +++ b/packages/visibility_detector/CHANGELOG.md @@ -1,36 +1,51 @@ -# 0.3.3 +# CHANGELOG + +## 0.4.0 + +* Refactor to avoid forcing composition in the layer/render trees. +* Remove `VisibilityDetectorLayer`. +* Add `RenderVisibilityDetectorBase` as a mixin that mostly takes over + functionality from the old layer. +* Remove the lookup map/method for getting former screen rects and instead add + the rect to `VisibilityInfo`. + +## 0.3.3 + * Re-apply Flutter framework bindings' null safety calls but set SDK constraints correctly to 2.12.0 instead. -# 0.3.2 +## 0.3.2 + * Reverts change from 0.3.0 where the Flutter version constraint should have been set to 2.12.0 instead of 2.10.5. -# 0.3.1-dev +## 0.3.1-dev + * Populate the pubspec `repository` field. -# 0.3.0 +## 0.3.0 + * Move to Flutter version 2.10.5 and update dependencies' null safety calls. -# 0.2.2 +## 0.2.2 * Minor internal changes to maintain forward-compatibility with [flutter#91753](https://github.com/flutter/flutter/pull/91753). -# 0.2.1 +## 0.2.1 * Bug fix for using VisibilityDetector with FittedBox and Transform.scale [issue #285](https://github.com/google/flutter.widgets/issues/285). -# 0.2.0 +## 0.2.0 * Added `SliverVisibilityDetector` to report visibility of `RenderSliver`-based widgets. Fixes [issue #174](https://github.com/google/flutter.widgets/issues/174). -# 0.2.0-nullsafety.1 +## 0.2.0-nullsafety.1 * Revert change to add `VisibilityDetectorController.scheduleNotification`, which introduced unexpected memory usage. -# 0.2.0-nullsafety.0 +## 0.2.0-nullsafety.0 * Update to null safety. @@ -41,7 +56,7 @@ * Add `VisibilityDetectorController.scheduleNotification` to force firing a visibility callback. -# 0.1.5 +## 0.1.5 * Compatibility fixes to `demo.dart` for Flutter 1.13.8. @@ -52,13 +67,13 @@ * Added a "Known limitations" section to `README.md`. -# 0.1.4 +## 0.1.4 * Style and comment adjustments. * Fix a potential infinite loop in the demo app and add tests for it. -# 0.1.3 +## 0.1.3 * Fixed positioning of text selection handles for `EditableText`-based widgets (e.g. `TextField`, `CupertinoTextField`) when used within a @@ -66,10 +81,10 @@ * Added `VisibilityDetectorController.widgetBoundsFor`. -# 0.1.2 +## 0.1.2 * Compatibility fixes for Flutter 1.3.0. -# 0.1.1 +## 0.1.1 * Added `VisibilityDetectorController.forget`. diff --git a/packages/visibility_detector/example/lib/main.dart b/packages/visibility_detector/example/lib/main.dart index 5348e12c..f9b800b8 100644 --- a/packages/visibility_detector/example/lib/main.dart +++ b/packages/visibility_detector/example/lib/main.dart @@ -74,6 +74,8 @@ class VisibilityDetectorDemo extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: title, + scrollBehavior: + const MaterialScrollBehavior().copyWith(scrollbars: false), theme: ThemeData(primarySwatch: Colors.blue), home: VisibilityDetectorDemoPage(key: key, useSlivers: useSlivers), ); diff --git a/packages/visibility_detector/lib/src/render_sliver_visibility_detector.dart b/packages/visibility_detector/lib/src/render_sliver_visibility_detector.dart deleted file mode 100644 index d1c2ab1c..00000000 --- a/packages/visibility_detector/lib/src/render_sliver_visibility_detector.dart +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2018 the Dart project authors. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; - -import 'visibility_detector.dart'; -import 'visibility_detector_layer.dart'; - -/// The [RenderObject] corresponding to the [SliverVisibilityDetector] widget. -/// -/// [RenderSliverVisibilityDetector] is a bridge between -/// [SliverVisibilityDetector] and [VisibilityDetectorLayer]. -class RenderSliverVisibilityDetector extends RenderProxySliver { - /// Constructor. See the corresponding properties for parameter details. - RenderSliverVisibilityDetector({ - RenderSliver? sliver, - required this.key, - required VisibilityChangedCallback? onVisibilityChanged, - }) : _onVisibilityChanged = onVisibilityChanged, - super(sliver); - - /// The key for the corresponding [VisibilityDetector] widget. - final Key key; - - VisibilityChangedCallback? _onVisibilityChanged; - - /// See [VisibilityDetector.onVisibilityChanged]. - VisibilityChangedCallback? get onVisibilityChanged => _onVisibilityChanged; - - /// Used by [VisibilityDetector.updateRenderObject]. - set onVisibilityChanged(VisibilityChangedCallback? value) { - _onVisibilityChanged = value; - markNeedsCompositingBitsUpdate(); - markNeedsPaint(); - } - - // See [RenderObject.alwaysNeedsCompositing]. - @override - bool get alwaysNeedsCompositing => onVisibilityChanged != null; - - /// See [RenderObject.paint]. - @override - void paint(PaintingContext context, Offset offset) { - if (onVisibilityChanged == null) { - // No need to create a [VisibilityDetectorLayer]. However, in case one - // already exists, remove all cached data for it so that we won't fire - // visibility callbacks when the layer is removed. - VisibilityDetectorLayer.forget(key); - super.paint(context, offset); - return; - } - - Size widgetSize; - Offset widgetOffset; - switch (applyGrowthDirectionToAxisDirection( - constraints.axisDirection, - constraints.growthDirection, - )) { - case AxisDirection.down: - widgetOffset = Offset(0, -constraints.scrollOffset); - widgetSize = Size(constraints.crossAxisExtent, geometry!.scrollExtent); - break; - case AxisDirection.up: - final startOffset = geometry!.paintExtent + - constraints.scrollOffset - - geometry!.scrollExtent; - widgetOffset = Offset(0, min(startOffset, 0)); - widgetSize = Size(constraints.crossAxisExtent, geometry!.scrollExtent); - break; - case AxisDirection.right: - widgetOffset = Offset(-constraints.scrollOffset, 0); - widgetSize = Size(geometry!.scrollExtent, constraints.crossAxisExtent); - break; - case AxisDirection.left: - final startOffset = geometry!.paintExtent + - constraints.scrollOffset - - geometry!.scrollExtent; - widgetOffset = Offset(min(startOffset, 0), 0); - widgetSize = Size(geometry!.scrollExtent, constraints.crossAxisExtent); - break; - } - - final layer = VisibilityDetectorLayer( - key: key, - widgetOffset: widgetOffset, - widgetSize: widgetSize, - paintOffset: offset, - onVisibilityChanged: onVisibilityChanged!); - context.pushLayer(layer, super.paint, offset); - } -} diff --git a/packages/visibility_detector/lib/src/render_visibility_detector.dart b/packages/visibility_detector/lib/src/render_visibility_detector.dart index 98ca160e..3dadd031 100644 --- a/packages/visibility_detector/lib/src/render_visibility_detector.dart +++ b/packages/visibility_detector/lib/src/render_visibility_detector.dart @@ -4,28 +4,93 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd +import 'dart:async'; +import 'dart:math' as math; + import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'visibility_detector.dart'; -import 'visibility_detector_layer.dart'; +import 'visibility_detector_controller.dart'; -/// The [RenderObject] corresponding to the [VisibilityDetector] widget. -/// -/// [RenderVisibilityDetector] is a bridge between [VisibilityDetector] and -/// [VisibilityDetectorLayer]. -class RenderVisibilityDetector extends RenderProxyBox { - /// Constructor. See the corresponding properties for parameter details. - RenderVisibilityDetector({ - RenderBox? child, - required this.key, - required VisibilityChangedCallback? onVisibilityChanged, - }) : assert(key != null), - _onVisibilityChanged = onVisibilityChanged, - super(child); +mixin RenderVisibilityDetectorBase on RenderObject { + static int? get debugUpdateCount { + if (!kDebugMode) { + return null; + } + return _updates.length; + } + + static Map _updates = {}; + static Map _lastVisibility = {}; + + /// See [VisibilityDetectorController.notifyNow]. + static void notifyNow() { + _timer?.cancel(); + _timer = null; + _processCallbacks(); + } + + static void forget(Key key) { + _updates.remove(key); + _lastVisibility.remove(key); + + if (_updates.isEmpty) { + _timer?.cancel(); + _timer = null; + } + } + + static Timer? _timer; + static void _handleTimer() { + _timer = null; + // Ensure that work is done between frames so that calculations are + // performed from a consistent state. We use `scheduleTask` here instead + // of `addPostFrameCallback` or `scheduleFrameCallback` so that work will + // be done even if a new frame isn't scheduled and without unnecessarily + // scheduling a new frame. + SchedulerBinding.instance.scheduleTask( + _processCallbacks, + Priority.touch, + ); + } + + /// Executes visibility callbacks for all updated instances. + static void _processCallbacks() { + for (final callback in _updates.values) { + callback(); + } + _updates.clear(); + } + + void _fireCallback(ContainerLayer? layer, Rect bounds) { + final oldInfo = _lastVisibility[key]; + final info = _determineVisibility(layer, bounds); + final visible = !info.visibleBounds.isEmpty; + + if (oldInfo == null) { + if (!visible) { + return; + } + } else if (info.matchesVisibility(oldInfo)) { + return; + } + + if (visible) { + _lastVisibility[key] = info; + } else { + // Track only visible items so that the map does not grow unbounded. + _lastVisibility.remove(key); + } + + onVisibilityChanged?.call(info); + } /// The key for the corresponding [VisibilityDetector] widget. - final Key key; + Key get key; + + VoidCallback? _compositionCallbackCanceller; VisibilityChangedCallback? _onVisibilityChanged; @@ -34,33 +99,220 @@ class RenderVisibilityDetector extends RenderProxyBox { /// Used by [VisibilityDetector.updateRenderObject]. set onVisibilityChanged(VisibilityChangedCallback? value) { + if (_onVisibilityChanged == value) { + return; + } + _compositionCallbackCanceller?.call(); + _compositionCallbackCanceller = null; _onVisibilityChanged = value; - markNeedsCompositingBitsUpdate(); - markNeedsPaint(); + + if (value == null) { + // Remove all cached data so that we won't fire visibility callbacks when + // a timer expires or get stale old information the next time around. + forget(key); + } else { + markNeedsPaint(); + // If an update is happening and some ancestor no longer paints this RO, + // the markNeedsPaint above will never cause the composition callback to + // fire and we could miss a hide event. This schedule will get + // over-written by subsequent updates in paint, if paint is called. + _scheduleUpdate(); + } } - // See [RenderObject.alwaysNeedsCompositing]. - @override - bool get alwaysNeedsCompositing => onVisibilityChanged != null; + int _debugScheduleUpdateCount = 0; + + /// The number of times the schedule update callback has been invoked from + /// [Layer.addCompositionCallback]. + /// + /// This is used for testing, and always returns null outside of debug mode. + @visibleForTesting + int? get debugScheduleUpdateCount { + if (kDebugMode) { + return _debugScheduleUpdateCount; + } + return null; + } + + void _scheduleUpdate([ContainerLayer? layer]) { + if (kDebugMode) { + _debugScheduleUpdateCount += 1; + } + bool isFirstUpdate = _updates.isEmpty; + _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 + // of the frame so that they're processed from a consistent state. This + // also ensures that they don't mutate the widget tree while we're in the + // middle of a frame. + if (isFirstUpdate) { + // We're about to render a frame, so a post-frame callback is guaranteed + // to fire and will give us the better immediacy than `scheduleTask`. + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + _processCallbacks(); + }); + } + } else if (_timer == null) { + // We use a normal [Timer] instead of a [RestartableTimer] so that changes + // to the update duration will be picked up automatically. + _timer = Timer(updateInterval, _handleTimer); + } else { + assert(_timer!.isActive); + } + } + + VisibilityInfo _determineVisibility(ContainerLayer? layer, Rect bounds) { + if (_disposed || layer?.attached == false || !attached) { + // layer is detached and thus invisible. + return VisibilityInfo( + key: key, + size: _lastVisibility[key]?.size ?? Size.zero, + ); + } + final transform = Matrix4.identity(); + + // Create a list of RenderObjects from this to the root, excluding the root + // since that has the DPR transform and we want to work with logical pixels. + // Cannot use the layer tree since some ancestor render object may have + // directly transformed/clipped the canvas. If there is some way to figure + // out how to get the RenderObjects below [layer], could take advantage of + // the usually shallower height of the layer tree compared to the render + // tree. Alternatively, if the canvas itself exposed the current matrix/clip + // we could use that. + RenderObject? ancestor = parent as RenderObject?; + + final List ancestors = []; + ancestors.add(this); + RenderObject child = this; + while (ancestor != null && ancestor.parent != null) { + if (!ancestor.paintsChild(child)) { + return VisibilityInfo(key: key, size: bounds.size); + } + ancestors.add(ancestor); + child = ancestor; + ancestor = ancestor.parent as RenderObject?; + } + + // Determine the transform and clip from first child of root down to + // this. + Rect clip = Rect.largest; + for (int index = ancestors.length - 1; index > 0; index -= 1) { + final parent = ancestors[index]; + final child = ancestors[index - 1]; + Rect? parentClip = parent.describeApproximatePaintClip(child); + if (parentClip != null) { + clip = clip.intersect(MatrixUtils.transformRect(transform, parentClip)); + } + parent.applyPaintTransform(child, transform); + } + return VisibilityInfo.fromRects( + key: key, + widgetBounds: MatrixUtils.transformRect(transform, bounds), + clipRect: clip, + ); + } + + /// Used to get the bounds of the render object when it is time to update + /// clients about visibility. + Rect get bounds; - /// See [RenderObject.paint]. @override void paint(PaintingContext context, Offset offset) { - if (onVisibilityChanged == null) { - // No need to create a [VisibilityDetectorLayer]. However, in case one - // already exists, remove all cached data for it so that we won't fire - // visibility callbacks when the layer is removed. - VisibilityDetectorLayer.forget(key); - super.paint(context, offset); - return; + if (onVisibilityChanged != null) { + _compositionCallbackCanceller?.call(); + _compositionCallbackCanceller = + context.addCompositionCallback((Layer layer) { + assert(!debugDisposed!); + final ContainerLayer? container = + layer is ContainerLayer ? layer : layer.parent; + _scheduleUpdate(container); + }); } + super.paint(context, offset); + } - final layer = VisibilityDetectorLayer( - key: key, - widgetOffset: Offset.zero, - widgetSize: semanticBounds.size, - paintOffset: offset, - onVisibilityChanged: onVisibilityChanged!); - context.pushLayer(layer, super.paint, offset); + bool _disposed = false; + @override + void dispose() { + _compositionCallbackCanceller?.call(); + _compositionCallbackCanceller = null; + _disposed = true; + super.dispose(); + } +} + +/// The [RenderObject] corresponding to the [VisibilityDetector] widget. +class RenderVisibilityDetector extends RenderProxyBox + with RenderVisibilityDetectorBase { + /// Constructor. See the corresponding properties for parameter details. + RenderVisibilityDetector({ + RenderBox? child, + required this.key, + required VisibilityChangedCallback? onVisibilityChanged, + }) : assert(key != null), + super(child) { + _onVisibilityChanged = onVisibilityChanged; + } + + @override + final Key key; + + @override + Rect get bounds => semanticBounds; +} + +/// The [RenderObject] corresponding to the [SliverVisibilityDetector] widget. +/// +/// [RenderSliverVisibilityDetector] is a bridge between +/// [SliverVisibilityDetector] and [VisibilityDetectorLayer]. +class RenderSliverVisibilityDetector extends RenderProxySliver + with RenderVisibilityDetectorBase { + /// Constructor. See the corresponding properties for parameter details. + RenderSliverVisibilityDetector({ + RenderSliver? sliver, + required this.key, + required VisibilityChangedCallback? onVisibilityChanged, + }) : super(sliver) { + _onVisibilityChanged = onVisibilityChanged; + } + + @override + final Key key; + + @override + Rect get bounds { + Size widgetSize; + Offset widgetOffset; + switch (applyGrowthDirectionToAxisDirection( + constraints.axisDirection, + constraints.growthDirection, + )) { + case AxisDirection.down: + widgetOffset = Offset(0, -constraints.scrollOffset); + widgetSize = Size(constraints.crossAxisExtent, geometry!.scrollExtent); + break; + case AxisDirection.up: + final startOffset = geometry!.paintExtent + + constraints.scrollOffset - + geometry!.scrollExtent; + widgetOffset = Offset(0, math.min(startOffset, 0)); + widgetSize = Size(constraints.crossAxisExtent, geometry!.scrollExtent); + break; + case AxisDirection.right: + widgetOffset = Offset(-constraints.scrollOffset, 0); + widgetSize = Size(geometry!.scrollExtent, constraints.crossAxisExtent); + break; + case AxisDirection.left: + final startOffset = geometry!.paintExtent + + constraints.scrollOffset - + geometry!.scrollExtent; + widgetOffset = Offset(math.min(startOffset, 0), 0); + widgetSize = Size(geometry!.scrollExtent, constraints.crossAxisExtent); + break; + } + return widgetOffset & widgetSize; } } diff --git a/packages/visibility_detector/lib/src/visibility_detector.dart b/packages/visibility_detector/lib/src/visibility_detector.dart index e4325dbb..f283b2f9 100644 --- a/packages/visibility_detector/lib/src/visibility_detector.dart +++ b/packages/visibility_detector/lib/src/visibility_detector.dart @@ -8,7 +8,6 @@ import 'dart:math' show max; import 'package:flutter/widgets.dart'; -import 'render_sliver_visibility_detector.dart'; import 'render_visibility_detector.dart'; /// A [VisibilityDetector] widget fires a specified callback when the widget @@ -104,13 +103,14 @@ class VisibilityInfo { /// `key` corresponds to the [Key] used to construct the corresponding /// [VisibilityDetector] widget. Must not be null. /// - /// If `size` or `visibleBounds` are omitted or null, the [VisibilityInfo] + /// If `size` or `visibleBounds` are omitted, the [VisibilityInfo] /// will be initialized to [Offset.zero] or [Rect.zero] respectively. This /// will indicate that the corresponding widget is competely hidden. - const VisibilityInfo({required this.key, Size? size, Rect? visibleBounds}) - : assert(key != null), - size = size ?? Size.zero, - visibleBounds = visibleBounds ?? Rect.zero; + const VisibilityInfo({ + required this.key, + this.size = Size.zero, + this.visibleBounds = Rect.zero, + }) : assert(key != null); /// Constructs a [VisibilityInfo] from widget bounds and a corresponding /// clipping rectangle. @@ -125,13 +125,17 @@ 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, visibleBounds: visibleBounds); + key: key, + size: widgetBounds.size, + visibleBounds: visibleBounds, + ); } /// The key for the corresponding [VisibilityDetector] widget. @@ -189,7 +193,18 @@ class VisibilityInfo { @override String toString() { - return 'VisibilityInfo(size: $size visibleBounds: $visibleBounds)'; + return 'VisibilityInfo(key: $key, size: $size visibleBounds: $visibleBounds)'; + } + + @override + int get hashCode => Object.hash(key, size, visibleBounds); + + @override + bool operator ==(Object other) { + return other is VisibilityInfo && + other.key == key && + other.size == size && + other.visibleBounds == visibleBounds; } } diff --git a/packages/visibility_detector/lib/src/visibility_detector_controller.dart b/packages/visibility_detector/lib/src/visibility_detector_controller.dart index a3583a83..a5047798 100644 --- a/packages/visibility_detector/lib/src/visibility_detector_controller.dart +++ b/packages/visibility_detector/lib/src/visibility_detector_controller.dart @@ -4,10 +4,9 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -import 'dart:ui' show Rect; import 'package:flutter/foundation.dart'; -import 'visibility_detector_layer.dart'; +import 'render_visibility_detector.dart'; /// A [VisibilityDetectorController] is a singleton object that can perform /// actions and change configuration for all [VisibilityDetector] widgets. @@ -29,7 +28,7 @@ class VisibilityDetectorController { /// /// This might be desirable just prior to tearing down the widget tree (such /// as when switching views or when exiting the application). - void notifyNow() => VisibilityDetectorLayer.notifyNow(); + void notifyNow() => RenderVisibilityDetectorBase.notifyNow(); /// Forgets any pending visibility callbacks for the [VisibilityDetector] with /// the given [key]. @@ -38,12 +37,12 @@ class VisibilityDetectorController { /// /// This method can be used to cancel timers after the [VisibilityDetector] /// has been detached to avoid pending timers in tests. - void forget(Key key) => VisibilityDetectorLayer.forget(key); + void forget(Key key) => RenderVisibilityDetectorBase.forget(key); - /// Returns the last known bounds for the [VisibilityDetector] with the given - /// [key] in global coordinates. - /// - /// Returns null if the specified [VisibilityDetector] is not visible or is - /// not found. - Rect? widgetBoundsFor(Key key) => VisibilityDetectorLayer.widgetBounds[key]; + int? get debugUpdateCount { + if (!kDebugMode) { + return null; + } + return RenderVisibilityDetectorBase.debugUpdateCount; + } } diff --git a/packages/visibility_detector/lib/src/visibility_detector_layer.dart b/packages/visibility_detector/lib/src/visibility_detector_layer.dart deleted file mode 100644 index 42f40bc5..00000000 --- a/packages/visibility_detector/lib/src/visibility_detector_layer.dart +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright 2018 the Dart project authors. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -import 'dart:async' show Timer; -import 'dart:ui' as ui; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; - -import 'visibility_detector.dart'; -import 'visibility_detector_controller.dart'; - -/// Returns a sequence containing the specified [Layer] and all of its -/// ancestors. The returned sequence is in [parent, child] order. -Iterable _getLayerChain(Layer start) { - final layerChain = []; - for (Layer? layer = start; layer != null; layer = layer.parent) { - layerChain.add(layer); - } - return layerChain.reversed; -} - -/// Returns the accumulated transform from the specified sequence of [Layer]s. -/// The sequence must be in [parent, child] order. The sequence must not be -/// null. -Matrix4 _accumulateTransforms(Iterable layerChain) { - assert(layerChain != null); - - final transform = Matrix4.identity(); - if (layerChain.isNotEmpty) { - var parent = layerChain.first; - for (final child in layerChain.skip(1)) { - (parent as ContainerLayer).applyTransform(child, transform); - parent = child; - } - } - return transform; -} - -/// Converts a [Rect] in local coordinates of the specified [Layer] to a new -/// [Rect] in global coordinates. -Rect _localRectToGlobal(Layer layer, Rect localRect) { - final layerChain = _getLayerChain(layer); - - // Skip the root layer which transforms from logical pixels to physical - // device pixels. - assert(layerChain.isNotEmpty); - assert(layerChain.first is TransformLayer); - final transform = _accumulateTransforms(layerChain.skip(1)); - return MatrixUtils.transformRect(transform, localRect); -} - -/// The [Layer] corresponding to a [VisibilityDetector] widget. -/// -/// We use a [Layer] because we can directly determine visibility by virtue of -/// being added to the [SceneBuilder]. -class VisibilityDetectorLayer extends ContainerLayer { - /// Constructor. See the corresponding properties for parameter details. - VisibilityDetectorLayer( - {required this.key, - required this.widgetOffset, - required this.widgetSize, - required this.paintOffset, - required this.onVisibilityChanged}) - : assert(key != null), - assert(paintOffset != null), - assert(widgetSize != null), - assert(onVisibilityChanged != null); - - /// Timer used by [_scheduleUpdate]. - static Timer? _timer; - - /// Keeps track of [VisibilityDetectorLayer] objects that have been recently - /// updated and that might need to report visibility changes. - /// - /// Additionally maps [VisibilityDetector] keys to the most recently added - /// [VisibilityDetectorLayer] that corresponds to it; this mapping is - /// necessary in case a layout change causes a new layer to be instantiated - /// for an existing key. - static final _updated = {}; - - /// Keeps track of the last known visibility state of a [VisibilityDetector]. - /// - /// This is used to suppress extraneous callbacks when visibility hasn't - /// changed. Stores entries only for visible [VisibilityDetector] objects; - /// entries for non-visible ones are actively removed. See [_fireCallback]. - static final _lastVisibility = {}; - - /// Keeps track of the last known bounds of a [VisibilityDetector], in global - /// coordinates. - static Map get widgetBounds => _lastBounds; - static final _lastBounds = {}; - - /// The key for the corresponding [VisibilityDetector] widget. - final Key key; - - /// Offset to the start of the widget, in local coordinates. - /// - /// This is zero for box widgets. For sliver widget, this offset points to - /// the start of the widget which may be outside the viewport. - final Offset widgetOffset; - - /// The size of the corresponding [VisibilityDetector] widget. - final Size widgetSize; - - /// The offset supplied to [RenderVisibilityDetector.paint] method. - final Offset paintOffset; - - /// See [VisibilityDetector.onVisibilityChanged]. - /// - /// Do not invoke this directly; call [_fireCallback] instead. - final VisibilityChangedCallback onVisibilityChanged; - - /// Computes the bounds for the corresponding [VisibilityDetector] widget, in - /// global coordinates. - Rect _computeWidgetBounds() { - return _localRectToGlobal(this, paintOffset + widgetOffset & widgetSize); - } - - /// Computes the accumulated clipping bounds, in global coordinates. - Rect _computeClipRect() { - assert(RendererBinding.instance.renderView != null); - var clipRect = Offset.zero & RendererBinding.instance.renderView.size; - - var parentLayer = parent; - while (parentLayer != null) { - Rect? curClipRect; - if (parentLayer is ClipRectLayer) { - curClipRect = parentLayer.clipRect; - } else if (parentLayer is ClipRRectLayer) { - curClipRect = parentLayer.clipRRect!.outerRect; - } else if (parentLayer is ClipPathLayer) { - curClipRect = parentLayer.clipPath!.getBounds(); - } - - if (curClipRect != null) { - // This is O(n^2) WRT the depth of the tree since `_localRectToGlobal` - // also walks up the tree. In practice there probably will be a small - // number of clipping layers in the chain, so it might not be a problem. - // Alternatively we could cache transformations and clipping rectangles. - curClipRect = _localRectToGlobal(parentLayer, curClipRect); - clipRect = clipRect.intersect(curClipRect); - } - - parentLayer = parentLayer.parent; - } - - return clipRect; - } - - /// Schedules a timer to invoke the visibility callbacks. The timer is used - /// to throttle and coalesce updates. - void _scheduleUpdate() { - final isFirstUpdate = _updated.isEmpty; - _updated[key] = this; - - final updateInterval = VisibilityDetectorController.instance.updateInterval; - if (updateInterval == Duration.zero) { - // Even with [Duration.zero], we still want to defer callbacks to the end - // of the frame so that they're processed from a consistent state. This - // also ensures that they don't mutate the widget tree while we're in the - // middle of a frame. - if (isFirstUpdate) { - // We're about to render a frame, so a post-frame callback is guaranteed - // to fire and will give us the better immediacy than `scheduleTask`. - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - _processCallbacks(); - }); - } - } else if (_timer == null) { - // We use a normal [Timer] instead of a [RestartableTimer] so that changes - // to the update duration will be picked up automatically. - _timer = Timer(updateInterval, _handleTimer); - } else { - assert(_timer!.isActive); - } - } - - /// [Timer] callback. Defers visibility callbacks to execute after the next - /// frame. - static void _handleTimer() { - _timer = null; - - // Ensure that work is done between frames so that calculations are - // performed from a consistent state. We use `scheduleTask` here instead - // of `addPostFrameCallback` or `scheduleFrameCallback` so that work will - // be done even if a new frame isn't scheduled and without unnecessarily - // scheduling a new frame. - SchedulerBinding.instance - .scheduleTask(_processCallbacks, Priority.touch); - } - - /// See [VisibilityDetectorController.notifyNow]. - static void notifyNow() { - _timer?.cancel(); - _timer = null; - _processCallbacks(); - } - - /// Removes entries corresponding to the specified [Key] from our internal - /// caches. - static void forget(Key key) { - _updated.remove(key); - _lastVisibility.remove(key); - _lastBounds.remove(key); - - if (_updated.isEmpty) { - _timer?.cancel(); - _timer = null; - } - } - - /// Executes visibility callbacks for all updated [VisibilityDetectorLayer] - /// instances. - static void _processCallbacks() { - for (final layer in _updated.values) { - if (!layer.attached) { - layer._fireCallback(VisibilityInfo( - key: layer.key, size: _lastVisibility[layer.key]?.size)); - continue; - } - - final widgetBounds = layer._computeWidgetBounds(); - _lastBounds[layer.key] = widgetBounds; - - final info = VisibilityInfo.fromRects( - key: layer.key, - widgetBounds: widgetBounds, - clipRect: layer._computeClipRect()); - layer._fireCallback(info); - } - _updated.clear(); - } - - /// Invokes the visibility callback if [VisibilityInfo] hasn't meaningfully - /// changed since the last time we invoked it. - void _fireCallback(VisibilityInfo info) { - assert(info != null); - - final oldInfo = _lastVisibility[key]; - final visible = !info.visibleBounds.isEmpty; - - if (oldInfo == null) { - if (!visible) { - return; - } - } else if (info.matchesVisibility(oldInfo)) { - return; - } - - if (visible) { - _lastVisibility[key] = info; - } else { - // Track only visible items so that the maps don't grow unbounded. - _lastVisibility.remove(key); - _lastBounds.remove(key); - } - - onVisibilityChanged(info); - } - - /// See [Layer.addToScene]. - @override - void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) { - // TODO(goderbauer): Remove unused layerOffset parameter once - // https://github.com/flutter/flutter/pull/91753 is in stable. - assert(layerOffset == Offset.zero); - _scheduleUpdate(); - super.addToScene(builder); - } - - /// See [AbstractNode.attach]. - @override - void attach(Object owner) { - super.attach(owner); - _scheduleUpdate(); - } - - /// See [AbstractNode.detach]. - @override - void detach() { - super.detach(); - - // The Layer might no longer be visible. We'll figure out whether it gets - // re-attached later. - _scheduleUpdate(); - } - - /// See [Diagnosticable.debugFillProperties]. - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - - properties - ..add(DiagnosticsProperty('key', key)) - ..add(DiagnosticsProperty('widgetRect', _computeWidgetBounds())) - ..add(DiagnosticsProperty('clipRect', _computeClipRect())); - } -} diff --git a/packages/visibility_detector/pubspec.yaml b/packages/visibility_detector/pubspec.yaml index 8fb837f0..5354d115 100644 --- a/packages/visibility_detector/pubspec.yaml +++ b/packages/visibility_detector/pubspec.yaml @@ -1,5 +1,5 @@ name: visibility_detector -version: 0.3.3 +version: 0.4.0 description: > A widget that detects the visibility of its child and notifies a callback. repository: https://github.com/google/flutter.widgets/tree/master/packages/visibility_detector diff --git a/packages/visibility_detector/test/impression_test.dart b/packages/visibility_detector/test/impression_test.dart new file mode 100644 index 00000000..42ae08b7 --- /dev/null +++ b/packages/visibility_detector/test/impression_test.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +void main() { + testWidgets('Material clip', (tester) async { + VisibilityDetectorController.instance.updateInterval = Duration.zero; + final Key listKey = UniqueKey(); + int onFirstVis = 0; + int onEnterVis = 0; + int onExitVis = 0; + bool inView = false; + await tester.pumpWidget( + MaterialApp( + home: Material( + child: SingleChildScrollView( + child: Column( + key: listKey, + children: [ + SizedBox.fromSize(size: Size(200, 1000)), + VisibilityDetector( + key: UniqueKey(), + onVisibilityChanged: (info) { + if (info.visibleFraction > .6) { + inView = true; + onFirstVis = 1; + onEnterVis += 1; + } else if (inView && info.visibleFraction < .4) { + onExitVis += 1; + inView = false; + } + }, + child: Container( + height: 200, + width: 300, + color: Colors.red, + ), + ), + ], + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(onFirstVis, 0); + expect(onEnterVis, 0); + expect(onExitVis, 0); + + final drags = [-1000.0, 1000.0, -1000.0, 1000.0, -1000.0]; + for (final dragAmount in drags) { + await tester.drag( + find.byType(SingleChildScrollView), Offset(0.0, dragAmount)); + await tester.pumpAndSettle(); + } + + expect(onFirstVis, 1); + expect(onEnterVis, 3); + expect(onExitVis, 2); + }); + + testWidgets('Material clip with intermediate ROs', (tester) async { + VisibilityDetectorController.instance.updateInterval = Duration.zero; + final Key listKey = UniqueKey(); + int onFirstVis = 0; + int onEnterVis = 0; + int onExitVis = 0; + bool inView = false; + await tester.pumpWidget( + MaterialApp( + home: Material( + child: SingleChildScrollView( + child: Column( + key: listKey, + children: [ + SizedBox.fromSize(size: Size(200, 1000)), + CustomPaint( + child: VisibilityDetector( + key: UniqueKey(), + onVisibilityChanged: (info) { + if (info.visibleFraction > .6) { + inView = true; + onFirstVis = 1; + onEnterVis += 1; + } else if (inView && info.visibleFraction < .4) { + onExitVis += 1; + inView = false; + } + }, + child: Container( + height: 200, + width: 300, + color: Colors.red, + ), + ), + ), + ], + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(onFirstVis, 0); + expect(onEnterVis, 0); + expect(onExitVis, 0); + + final drags = [-1000.0, 1000.0, -1000.0, 1000.0, -1000.0]; + for (final dragAmount in drags) { + await tester.drag( + find.byType(SingleChildScrollView), Offset(0.0, dragAmount)); + await tester.pumpAndSettle(); + } + + expect(onFirstVis, 1); + expect(onEnterVis, 3); + expect(onExitVis, 2); + }); + + testWidgets('Programmatic visibility change', (WidgetTester tester) async { + VisibilityDetectorController.instance.updateInterval = Duration.zero; + final List infos = []; + await tester.pumpWidget( + VisibilityDetector( + key: Key('app_widget'), + onVisibilityChanged: (info) { + infos.add(info); + }, + child: Visibility( + maintainState: true, + visible: true, + child: VisibilityDetector( + key: Key('weatherCard'), + onVisibilityChanged: (info) { + infos.add(info); + }, + child: Placeholder(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.pumpWidget( + VisibilityDetector( + key: Key('app_widget'), + onVisibilityChanged: (info) { + infos.add(info); + }, + child: Visibility( + maintainState: true, + visible: false, + child: VisibilityDetector( + key: Key('weatherCard'), + onVisibilityChanged: (info) { + infos.add(info); + }, + child: Placeholder(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.pumpWidget( + VisibilityDetector( + key: Key('app_widget'), + onVisibilityChanged: (info) { + infos.add(info); + }, + child: Visibility( + maintainState: true, + visible: true, + child: VisibilityDetector( + key: Key('weatherCard'), + onVisibilityChanged: (info) { + infos.add(info); + }, + child: Placeholder(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.pumpWidget(Placeholder()); + await tester.pumpAndSettle(); + + expect(infos, const [ + VisibilityInfo( + key: Key('app_widget'), + size: Size(800, 600), + visibleBounds: Rect.fromLTRB(0, 0, 800, 600), + ), + VisibilityInfo( + key: Key('weatherCard'), + size: Size(800, 600), + visibleBounds: Rect.fromLTRB(0, 0, 800, 600), + ), + VisibilityInfo( + key: Key('weatherCard'), + size: Size(800, 600), + ), + VisibilityInfo( + key: Key('weatherCard'), + size: Size(800, 600), + visibleBounds: Rect.fromLTRB(0, 0, 800, 600), + ), + VisibilityInfo( + key: Key('app_widget'), + size: Size(800, 600), + ), + VisibilityInfo( + key: Key('weatherCard'), + size: Size(800, 600), + ), + ]); + }); +} diff --git a/packages/visibility_detector/test/render_visibility_detector_test.dart b/packages/visibility_detector/test/render_visibility_detector_test.dart new file mode 100644 index 00000000..f636d149 --- /dev/null +++ b/packages/visibility_detector/test/render_visibility_detector_test.dart @@ -0,0 +1,132 @@ +// Copyright 2018 the Dart project authors. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +import 'dart:ui'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:visibility_detector/src/render_visibility_detector.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +void main() { + VisibilityDetectorController.instance.updateInterval = Duration.zero; + + testWidgets('RVS (box) unregisters its callback on paint', + (WidgetTester tester) async { + final RenderVisibilityDetector detector = RenderVisibilityDetector( + key: Key('test'), + onVisibilityChanged: (_) {}, + ); + + final ContainerLayer layer = ContainerLayer(); + final PaintingContext context = PaintingContext(layer, Rect.largest); + expect(layer.subtreeHasCompositionCallbacks, false); + + detector.layout(BoxConstraints.tight(const Size(200, 200))); + detector.paint(context, Offset.zero); + detector.paint(context, Offset.zero); + expect(layer.subtreeHasCompositionCallbacks, true); + + expect(detector.debugScheduleUpdateCount, 0); + layer.buildScene(SceneBuilder()).dispose(); + + expect(detector.debugScheduleUpdateCount, 1); + }); + + testWidgets('RVS (sliver) unregisters its callback on paint', + (WidgetTester tester) async { + final RenderSliverVisibilityDetector detector = + RenderSliverVisibilityDetector( + key: Key('test'), + onVisibilityChanged: (_) {}, + sliver: RenderSliverToBoxAdapter(child: RenderLimitedBox()), + ); + + final ContainerLayer layer = ContainerLayer(); + final PaintingContext context = PaintingContext(layer, Rect.largest); + expect(layer.subtreeHasCompositionCallbacks, false); + + detector.layout(SliverConstraints( + axisDirection: AxisDirection.down, + growthDirection: GrowthDirection.forward, + userScrollDirection: ScrollDirection.forward, + scrollOffset: 0, + precedingScrollExtent: 0, + overlap: 0, + remainingPaintExtent: 0, + crossAxisExtent: 0, + crossAxisDirection: AxisDirection.left, + viewportMainAxisExtent: 0, + remainingCacheExtent: 0, + cacheOrigin: 0, + )); + + final owner = PipelineOwner(); + detector.attach(owner); + owner.flushCompositingBits(); + + detector.paint(context, Offset.zero); + detector.paint(context, Offset.zero); + expect(layer.subtreeHasCompositionCallbacks, true); + + expect(detector.debugScheduleUpdateCount, 0); + layer.buildScene(SceneBuilder()).dispose(); + + expect(detector.debugScheduleUpdateCount, 1); + }); + + testWidgets('RVS unregisters its callback on dispose', + (WidgetTester tester) async { + final RenderVisibilityDetector detector = RenderVisibilityDetector( + key: Key('test'), + onVisibilityChanged: (_) {}, + ); + + final ContainerLayer layer = ContainerLayer(); + final PaintingContext context = PaintingContext(layer, Rect.largest); + expect(layer.subtreeHasCompositionCallbacks, false); + + detector.layout(BoxConstraints.tight(const Size(200, 200))); + + detector.paint(context, Offset.zero); + expect(layer.subtreeHasCompositionCallbacks, true); + + detector.dispose(); + expect(layer.subtreeHasCompositionCallbacks, false); + + expect(detector.debugScheduleUpdateCount, 0); + layer.buildScene(SceneBuilder()).dispose(); + + expect(detector.debugScheduleUpdateCount, 0); + }); + + testWidgets('RVS unregisters its callback when callback changes', + (WidgetTester tester) async { + final RenderVisibilityDetector detector = RenderVisibilityDetector( + key: Key('test'), + onVisibilityChanged: (_) {}, + ); + + final ContainerLayer layer = ContainerLayer(); + final PaintingContext context = PaintingContext(layer, Rect.largest); + expect(layer.subtreeHasCompositionCallbacks, false); + + detector.layout(BoxConstraints.tight(const Size(200, 200))); + + detector.paint(context, Offset.zero); + expect(layer.subtreeHasCompositionCallbacks, true); + + detector.onVisibilityChanged = null; + + expect(layer.subtreeHasCompositionCallbacks, false); + + expect(detector.debugScheduleUpdateCount, 0); + layer.buildScene(SceneBuilder()).dispose(); + + expect(detector.debugScheduleUpdateCount, 0); + }); +} diff --git a/packages/visibility_detector/test/widget_test.dart b/packages/visibility_detector/test/widget_test.dart index e26503b1..386e552b 100644 --- a/packages/visibility_detector/test/widget_test.dart +++ b/packages/visibility_detector/test/widget_test.dart @@ -16,9 +16,6 @@ final _positionToVisibilityInfo = {}; /// [Key] used to identify the [_TestPropertyChange] widget. final _testPropertyChangeKey = GlobalKey<_TestPropertyChangeState>(); -/// [Key] used to identify the [_TestOffset] widget or its [VisibilityDetector]. -final _testOffsetKey = UniqueKey(); - void main() { setUpAll(() { demo.visibilityListeners.add((demo.RowColumn rc, VisibilityInfo info) { @@ -41,7 +38,6 @@ void main() { _wrapTest( 'VisibilityDetector reports initial visibility', callback: (tester) async { - final cellKey = demo.cellKey(0, 0); final expectedRect = tester.getRect(find.byKey(demo.cellContentKey(0, 0))); var info = _positionToVisibilityInfo[demo.RowColumn(0, 0)]; @@ -53,10 +49,6 @@ void main() { expect(info.size.height, demo.cellHeight); expect(info.visibleBounds, Offset.zero & info.size); expect(info.visibleFraction, 1.0); - - final bounds = - VisibilityDetectorController.instance.widgetBoundsFor(cellKey); - expect(bounds, expectedRect); }, ); @@ -84,7 +76,6 @@ void main() { expect(mainList, findsOneWidget); final viewRect = tester.getRect(mainList); - final cellKey = demo.cellKey(0, 0); final originalRect = tester.getRect(find.byKey(demo.cellContentKey(0, 0))); @@ -106,10 +97,6 @@ void main() { expect(info.visibleBounds, expectedVisibleBounds); expect(info.visibleFraction, info.visibleBounds.height / originalRect.height); - - final bounds = - VisibilityDetectorController.instance.widgetBoundsFor(cellKey); - expect(bounds, originalRect.shift(const Offset(0, -dy))); }, ); @@ -120,7 +107,6 @@ void main() { final mainList = find.byKey(demo.mainListKey); final viewRect = tester.getRect(mainList); - final cellKey = demo.cellKey(2, 0); final originalRect = tester.getRect(find.byKey(demo.cellContentKey(2, 0))); const dx = 30.0; @@ -143,10 +129,6 @@ void main() { expect(info.visibleBounds, expectedVisibleBounds); expect( info.visibleFraction, info.visibleBounds.width / originalRect.width); - - final bounds = - VisibilityDetectorController.instance.widgetBoundsFor(cellKey); - expect(bounds, originalRect.shift(const Offset(-dx, 0))); }, ); @@ -158,7 +140,6 @@ void main() { expect(mainList, findsOneWidget); final viewRect = tester.getRect(mainList); - final cellKey = demo.cellKey(0, 0); final originalRect = tester.getRect(find.byKey(demo.cellContentKey(0, 0))); @@ -172,10 +153,6 @@ void main() { expect(info.size, originalRect.size); expect(info.visibleBounds.size, Size.zero); expect(info.visibleFraction, 0.0); - - final bounds = - VisibilityDetectorController.instance.widgetBoundsFor(cellKey); - expect(bounds, null); }, ); @@ -187,7 +164,6 @@ void main() { expect(mainList, findsOneWidget); final viewRect = tester.getRect(mainList); - final cellKey = demo.cellKey(0, 0); final originalRect = tester.getRect(find.byKey(demo.cellContentKey(0, 0))); @@ -207,10 +183,6 @@ void main() { expect(info.visibleBounds, expectedVisibleBounds); expect(info.visibleFraction, info.visibleBounds.height / originalRect.height); - - final bounds = - VisibilityDetectorController.instance.widgetBoundsFor(cellKey); - expect(bounds, originalRect.shift(Offset(0, -dy))); }, ); @@ -218,7 +190,6 @@ void main() { 'VisibilityDetector reports being not visible when removed from the widget ' 'tree', callback: (tester) async { - final cellKey = demo.cellKey(0, 0); final originalRect = tester.getRect(find.byKey(demo.cellContentKey(0, 0))); @@ -231,10 +202,6 @@ void main() { expect(info.size, originalRect.size); expect(info.visibleBounds.size, Size.zero); expect(info.visibleFraction, 0.0); - - final bounds = - VisibilityDetectorController.instance.widgetBoundsFor(cellKey); - expect(bounds, null); }, ); @@ -327,29 +294,6 @@ void main() { _expectVisibility(demo.RowColumn(5, 0), 0, epsilon: 0); }, ); - - _wrapTest( - 'VisibilityDetector computes widget bounds in global coordinates', - widget: _TestOffset(key: _testOffsetKey), - callback: (tester) async { - final viewSize = tester.binding.renderView.size; - - final bounds = - VisibilityDetectorController.instance.widgetBoundsFor(_testOffsetKey); - expect( - bounds, - tester.getRect(find.byType(VisibilityDetector)), - ); - expect( - bounds, - Rect.fromCenter( - center: viewSize.center(Offset.zero), - width: _TestOffset.detectorWidth, - height: _TestOffset.detectorHeight, - ), - ); - }, - ); } /// Initializes the widget tree that is populated with [VisibilityDetector] @@ -410,7 +354,7 @@ void _wrapTest( }); // Test one more time using slivers version of the demo. - testWidgets(description, (tester) async { + testWidgets('$description (slivers)', (tester) async { await _initWidgetTree( widget ?? demo.VisibilityDetectorDemo(useSlivers: true), tester, @@ -532,31 +476,3 @@ class _TestPropertyChangeState extends State<_TestPropertyChange> { ); } } - -/// A widget to exercise calling [RenderVisibilityDetector.paint] with a -/// non-zero [Offset]. -class _TestOffset extends StatelessWidget { - const _TestOffset({required Key key}) : super(key: key); - - static const detectorWidth = 200.0; - static const detectorHeight = 100.0; - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - body: Center( - child: VisibilityDetector( - key: key!, - onVisibilityChanged: (visibilityInfo) {}, - child: const SizedBox( - width: detectorWidth, - height: detectorHeight, - child: Placeholder(), - ), - ), - ), - ), - ); - } -}