Skip to content

Commit

Permalink
Copybara sync (#4)
Browse files Browse the repository at this point in the history
* 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
jamesderlin committed Dec 1, 2018
1 parent 148fcbe commit 5e483f2
Show file tree
Hide file tree
Showing 12 changed files with 1,484 additions and 1 deletion.
6 changes: 6 additions & 0 deletions gallery/lib/gallery.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'package:meta/meta.dart';
import 'package:gallery/src/linked_scroll_controller_page.dart';
import 'package:gallery/src/tagged_text_page.dart';
import 'package:gallery/src/html_widget_page.dart' as html_latency;
import 'package:flutter_widgets/flutter_widgets.dart'
show VisibilityDetectorDemoPage;

/// Router to all widgets inside the gallery app.
///
Expand All @@ -25,6 +27,10 @@ class Gallery extends StatefulWidget {
..add(new _GalleryPage(
title: 'Linked Scrollables',
pageBuilder: (context) => new LinkedScrollablesPage(),
))
..add(_GalleryPage(
title: 'Visibility Detector',
pageBuilder: (context) => VisibilityDetectorDemoPage(),
));

@override
Expand Down
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);
}
}
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);
}
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();
}
}
Loading

0 comments on commit 5e483f2

Please sign in to comment.