-
Notifications
You must be signed in to change notification settings - Fork 503
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Export the linked scroll controller PiperOrigin-RevId: 220713242 * Add a Flutter VisibilityDetector widget A VisibilityDetector widget wraps an existing Flutter widget and fires a callback when the widget's visibility changes. (We actually report when the visibility of the VisibilityDetector itself changes, and its visibility is expected to be identical to that of its child.) Implementation notes: * The VisibilityDetector widget's `Key` is required. This is necessary to associate the VisibilityDetector widget with a specific RenderObject and Layer. Without a key, layout changes could trigger unwanted visibility callbacks and report that VisibilityDetector widgets suddenly became hidden and then visible again. * The VisibilityDetector key must be globally unique across all VisibilityDetector widgets in the app. However, I used a normal `Key` instead of a `GlobalKey` to avoid forcing clients to use a specific `Key` implementation. (For example, `UniqueKey` is globally unique but derives from `LocalKey`.) * Add a page to the Gallery to demonstrate the `VisibilityDetector` widget. * Add widget tests for `VisibilityDetector`. The widget tree is non-trivial, so instead of duplicating code, make the tests leverage the Gallery page. PiperOrigin-RevId: 221373750 PiperOrigin-RevId: 221375140 PiperOrigin-RevId: 221505135 PiperOrigin-RevId: 222451874 PiperOrigin-RevId: 222854998 * Update the pubspec.yaml to publish to pub. PiperOrigin-RevId: 223190729
- Loading branch information
1 parent
148fcbe
commit 1ae8816
Showing
12 changed files
with
1,485 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
...ird_party/dart_src/acx_mobile/visibility_detector/lib/src/render_visibility_detector.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// 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 'package:flutter/foundation.dart'; | ||
import 'package:flutter/rendering.dart'; | ||
|
||
import 'visibility_detector.dart'; | ||
import 'visibility_detector_layer.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 onVisibilityChanged, | ||
}) : _onVisibilityChanged = onVisibilityChanged, | ||
super(child) { | ||
assert(key != null); | ||
} | ||
|
||
/// The key for the corresponding [VisibilityDetector] widget. Never null. | ||
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; | ||
} | ||
|
||
final layer = VisibilityDetectorLayer( | ||
key: key, | ||
widgetSize: semanticBounds.size, | ||
paintOffset: offset, | ||
onVisibilityChanged: onVisibilityChanged); | ||
// We'll apply the offset in the [VisibilityDetectorLayer] instead of in the | ||
// [PaintingContext]. | ||
context.pushLayer(layer, super.paint, Offset.zero); | ||
} | ||
} |
166 changes: 166 additions & 0 deletions
166
google3/third_party/dart_src/acx_mobile/visibility_detector/lib/src/visibility_detector.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// 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' show max; | ||
|
||
import 'package:flutter/rendering.dart'; | ||
import 'package:flutter/widgets.dart'; | ||
|
||
import 'render_visibility_detector.dart'; | ||
|
||
/// A [VisibilityDetector] widget fires a specified callback when the widget | ||
/// changes visibility. | ||
/// | ||
/// Callbacks are not fired immediately on visibility changes. Instead, | ||
/// callbacks are deferred and coalesced such that the callback for each | ||
/// [VisibilityDetector] will be invoked at most once per | ||
/// [VisibilityDetectorController.updateInterval] (unless forced by | ||
/// [VisibilityDetectorController.notifyNow]). Callbacks for *all* | ||
/// [VisibilityDetector] widgets are fired together synchronously between | ||
/// frames. | ||
class VisibilityDetector extends SingleChildRenderObjectWidget { | ||
/// Constructor. | ||
/// | ||
/// `key` is required to properly identify this widget; it must be unique | ||
/// among all [VisibilityDetector] widgets. | ||
/// | ||
/// `child` must not be null. | ||
/// | ||
/// `onVisibilityChanged` may be null to disable this [VisibilityDetector]. | ||
VisibilityDetector({ | ||
@required Key key, | ||
@required Widget child, | ||
@required this.onVisibilityChanged, | ||
}) : super(key: key, child: child) { | ||
assert(key != null); | ||
assert(child != null); | ||
} | ||
|
||
/// The callback to invoke when this widget's visibility changes. | ||
final VisibilityChangedCallback onVisibilityChanged; | ||
|
||
/// See [RenderObjectWidget.createRenderObject]. | ||
@override | ||
RenderVisibilityDetector createRenderObject(BuildContext context) { | ||
return RenderVisibilityDetector( | ||
key: key, | ||
onVisibilityChanged: onVisibilityChanged, | ||
); | ||
} | ||
|
||
/// See [RenderObjectWidget.updateRenderObject]. | ||
@override | ||
void updateRenderObject( | ||
BuildContext context, RenderVisibilityDetector renderObject) { | ||
assert(renderObject.key == key); | ||
renderObject.onVisibilityChanged = onVisibilityChanged; | ||
} | ||
} | ||
|
||
typedef VisibilityChangedCallback = void Function(VisibilityInfo info); | ||
|
||
/// Data passed to the [VisibilityDetector.onVisibilityChanged] callback. | ||
@immutable | ||
class VisibilityInfo { | ||
/// Constructor. | ||
/// | ||
/// `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] | ||
/// will be initialized to [Offset.zero] or [Rect.zero] respectively. This | ||
/// will indicate that the corresponding widget is competely hidden. | ||
VisibilityInfo({@required this.key, Size size, Rect visibleBounds}) | ||
: size = size ?? Size.zero, | ||
visibleBounds = visibleBounds ?? Rect.zero { | ||
assert(key != null); | ||
} | ||
|
||
factory VisibilityInfo.fromRects({ | ||
@required Key key, | ||
@required Rect widgetBounds, | ||
@required Rect clipRect, | ||
}) { | ||
assert(widgetBounds != null); | ||
assert(clipRect != null); | ||
|
||
// Compute the intersection in the widget's local coordinates. | ||
final Rect visibleBounds = widgetBounds.overlaps(clipRect) | ||
? widgetBounds.intersect(clipRect).shift(-widgetBounds.topLeft) | ||
: Rect.zero; | ||
|
||
return VisibilityInfo( | ||
key: key, size: widgetBounds.size, visibleBounds: visibleBounds); | ||
} | ||
|
||
/// The key for the corresponding [VisibilityDetector] widget. Never null. | ||
final Key key; | ||
|
||
/// The size of the widget. Never null. | ||
final Size size; | ||
|
||
/// The visible portion of the widget, in the widget's local coordinates. | ||
/// Never null. | ||
final Rect visibleBounds; | ||
|
||
/// A fraction in the range [0, 1] that represents what proportion of the | ||
/// widget is visible (assuming rectangular bounding boxes). 0 means not | ||
/// visible; 1 means fully visible. | ||
double get visibleFraction { | ||
final double visibleArea = _area(visibleBounds.size); | ||
final double maxVisibleArea = _area(size); | ||
|
||
if (_floatNear(maxVisibleArea, 0)) { | ||
// Avoid division-by-zero. | ||
return 0; | ||
} | ||
|
||
double visibleFraction = visibleArea / maxVisibleArea; | ||
|
||
if (_floatNear(visibleFraction, 0)) { | ||
visibleFraction = 0; | ||
} else if (_floatNear(visibleFraction, 1)) { | ||
// The inexact nature of floating-point arithmetic means that sometimes | ||
// the visible area might never equal the maximum area (or could even | ||
// be slightly larger than the maximum). Snap to the maximum. | ||
visibleFraction = 1; | ||
} | ||
|
||
assert(visibleFraction >= 0); | ||
assert(visibleFraction <= 1); | ||
return visibleFraction; | ||
} | ||
|
||
/// Returns true if the specified [VisibilityInfo] object has equivalent | ||
/// visibility to this one. | ||
bool matchesVisibility(VisibilityInfo info) { | ||
// We don't override `operator ==` so that object equality can be separate | ||
// from whether two [VisibilityInfo] objects are sufficiently similar | ||
// that we don't need to fire callbacks for both. This could be pertinent | ||
// if other properties are added. | ||
assert(info != null); | ||
return size == info.size && visibleBounds == info.visibleBounds; | ||
} | ||
} | ||
|
||
/// The tolerance used to determine whether two floating-point values are | ||
/// approximately equal. | ||
const _kDefaultTolerance = 0.01; | ||
|
||
/// Computes the area of a rectangle of the specified dimensions. | ||
double _area(Size size) { | ||
assert(size != null); | ||
assert(size.width >= 0); | ||
assert(size.height >= 0); | ||
return size.width * size.height; | ||
} | ||
|
||
/// Returns whether two floating-point values are approximately equal. | ||
bool _floatNear(double f1, double f2) { | ||
double absDiff = (f1 - f2).abs(); | ||
return absDiff <= _kDefaultTolerance || | ||
(absDiff / max(f1.abs(), f2.abs()) <= _kDefaultTolerance); | ||
} |
32 changes: 32 additions & 0 deletions
32
...party/dart_src/acx_mobile/visibility_detector/lib/src/visibility_detector_controller.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// 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 'visibility_detector_layer.dart'; | ||
|
||
/// A [VisibilityDetectorController] is a singleton object that can perform | ||
/// actions and change configuration for all [VisibilityDetector] widgets. | ||
class VisibilityDetectorController { | ||
static final _instance = VisibilityDetectorController(); | ||
static VisibilityDetectorController get instance => _instance; | ||
|
||
/// The minimum amount of time to wait between firing batches of visibility | ||
/// callbacks. | ||
/// | ||
/// If set to [Duration.zero], callbacks instead will fire at the end of every | ||
/// frame. This is useful for automated tests. | ||
/// | ||
/// Changing [updateInterval] will not affect any pending callbacks. Clients | ||
/// should call [notifyNow] explicitly to flush them if desired. | ||
Duration updateInterval = Duration(milliseconds: 500); | ||
|
||
/// Forces firing all pending visibility callbacks immmediately. | ||
/// | ||
/// 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(); | ||
} | ||
} |
Oops, something went wrong.