You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
VisibilityDetector uses RenderProxy, and can not be used to detect when a sliver appears/dissapears.
It would be amazing to have a SliverVisibilityDetector.
import'package:flutter/rendering.dart';
import'package:flutter/widgets.dart';
import'package:visibility_detector/visibility_detector.dart';
import'package:visibility_detector/src/visibility_detector_layer.dart';
/// A [SliverVisibilityDetector] widget fires a specified callback when the sliver/// 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.classSliverVisibilityDetectorextendsSingleChildRenderObjectWidget {
/// Constructor. /// /// `key` is required to properly identify this widget; it must be unique /// among all [SliverVisibilityDetector] widgets. /// /// `child` must not be null. /// /// `onVisibilityChanged` may be null to disable this [VisibilityDetector].constSliverVisibilityDetector({
@requiredKey key,
@requiredWidget sliver,
@requiredthis.onVisibilityChanged,
}) :assert(key !=null),
assert(sliver !=null),
super(key: key, child: sliver);
/// The callback to invoke when this widget's visibility changes.finalVisibilityChangedCallback onVisibilityChanged;
/// See [RenderObjectWidget.createRenderObject].@overrideRenderSliverVisibilityDetectorcreateRenderObject(BuildContext context) {
returnRenderSliverVisibilityDetector(
key: key,
onVisibilityChanged: onVisibilityChanged,
);
}
/// See [RenderObjectWidget.updateRenderObject].@overridevoidupdateRenderObject(
BuildContext context, RenderSliverVisibilityDetector renderObject) {
assert(renderObject.key == key);
renderObject.onVisibilityChanged = onVisibilityChanged;
}
}
/// The [RenderSliver] corresponding to the [VisibilityDetector] widget.////// [RenderVisibilityDetector] is a bridge between [VisibilityDetector] and/// [VisibilityDetectorLayer].classRenderSliverVisibilityDetectorextendsRenderProxySliver {
/// Constructor. See the corresponding properties for parameter details.RenderSliverVisibilityDetector({
RenderSliver child,
@requiredthis.key,
@requiredVisibilityChangedCallback onVisibilityChanged,
}) :assert(key !=null),
_onVisibilityChanged = onVisibilityChanged,
super(child);
/// The key for the corresponding [VisibilityDetector] widget. Never null.finalKey key;
VisibilityChangedCallback _onVisibilityChanged;
/// See [VisibilityDetector.onVisibilityChanged].VisibilityChangedCallbackget onVisibilityChanged => _onVisibilityChanged;
/// Used by [VisibilityDetector.updateRenderObject].setonVisibilityChanged(VisibilityChangedCallback value) {
_onVisibilityChanged = value;
markNeedsCompositingBitsUpdate();
markNeedsPaint();
}
// See [RenderObject.alwaysNeedsCompositing].@overrideboolget alwaysNeedsCompositing => onVisibilityChanged !=null;
/// See [RenderObject.paint].@overridevoidpaint(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:getWidgetSize(),
paintOffset: offset,
onVisibilityChanged: onVisibilityChanged);
context.pushLayer(layer, super.paint, offset);
}
@protectedSizegetWidgetSize() {
assert(geometry !=null);
assert(!debugNeedsLayout);
switch (constraints.axisDirection) {
caseAxisDirection.up:caseAxisDirection.down:returnSize(constraints.crossAxisExtent, geometry.scrollExtent);
caseAxisDirection.right:caseAxisDirection.left:returnSize(geometry.scrollExtent, constraints.crossAxisExtent);
}
returnnull;
}
}
The only important change for a SliverVisibilityDetector would be to use RenderSliverProxy and calculate the widgetSize with the geometry.scrollExtent. Right now RenderVisibilityDetector uses semanticBounds, that in RenderSliver uses geometry.paintExtent and doesn't correspond with the full size of the widget and only the part that is being painted.
Let me know if you would be interested to implement this or accept a PR. Otherwise I will make a package so it can be available to other developers
Full Example with SliverVisibilityDetector
// 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/bsdimport'dart:collection';
import'package:flutter/material.dart';
import'package:visibility_detector/visibility_detector.dart';
import'package:flutter/rendering.dart';
import'package:flutter/widgets.dart';
import'package:visibility_detector/src/visibility_detector_layer.dart';
/// A [SliverVisibilityDetector] widget fires a specified callback when the sliver/// 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.classSliverVisibilityDetectorextendsSingleChildRenderObjectWidget {
/// Constructor. /// /// `key` is required to properly identify this widget; it must be unique /// among all [SliverVisibilityDetector] widgets. /// /// `child` must not be null. /// /// `onVisibilityChanged` may be null to disable this [VisibilityDetector].constSliverVisibilityDetector({
@requiredKey key,
@requiredWidget sliver,
@requiredthis.onVisibilityChanged,
}) :assert(key !=null),
assert(sliver !=null),
super(key: key, child: sliver);
/// The callback to invoke when this widget's visibility changes.finalVisibilityChangedCallback onVisibilityChanged;
/// See [RenderObjectWidget.createRenderObject].@overrideRenderSliverVisibilityDetectorcreateRenderObject(BuildContext context) {
returnRenderSliverVisibilityDetector(
key: key,
onVisibilityChanged: onVisibilityChanged,
);
}
/// See [RenderObjectWidget.updateRenderObject].@overridevoidupdateRenderObject(
BuildContext context, RenderSliverVisibilityDetector renderObject) {
assert(renderObject.key == key);
renderObject.onVisibilityChanged = onVisibilityChanged;
}
}
/// The [RenderSliver] corresponding to the [VisibilityDetector] widget.////// [RenderVisibilityDetector] is a bridge between [VisibilityDetector] and/// [VisibilityDetectorLayer].classRenderSliverVisibilityDetectorextendsRenderProxySliver {
/// Constructor. See the corresponding properties for parameter details.RenderSliverVisibilityDetector({
RenderSliver child,
@requiredthis.key,
@requiredVisibilityChangedCallback onVisibilityChanged,
}) :assert(key !=null),
_onVisibilityChanged = onVisibilityChanged,
super(child);
/// The key for the corresponding [VisibilityDetector] widget. Never null.finalKey key;
VisibilityChangedCallback _onVisibilityChanged;
/// See [VisibilityDetector.onVisibilityChanged].VisibilityChangedCallbackget onVisibilityChanged => _onVisibilityChanged;
/// Used by [VisibilityDetector.updateRenderObject].setonVisibilityChanged(VisibilityChangedCallback value) {
_onVisibilityChanged = value;
markNeedsCompositingBitsUpdate();
markNeedsPaint();
}
// See [RenderObject.alwaysNeedsCompositing].@overrideboolget alwaysNeedsCompositing => onVisibilityChanged !=null;
/// See [RenderObject.paint].@overridevoidpaint(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:getWidgetSize(),
paintOffset: offset,
onVisibilityChanged: onVisibilityChanged);
context.pushLayer(layer, super.paint, offset);
}
@protectedSizegetWidgetSize() {
assert(geometry !=null);
assert(!debugNeedsLayout);
switch (constraints.axisDirection) {
caseAxisDirection.up:caseAxisDirection.down:returnSize(constraints.crossAxisExtent, geometry.scrollExtent);
caseAxisDirection.right:caseAxisDirection.left:returnSize(geometry.scrollExtent, constraints.crossAxisExtent);
}
returnnull;
}
}
/// EXAMPLEconstString title ='VisibilityDetector Demo';
/// The height of each row of the pseudo-table. This includes [_rowPadding] on/// top and bottom.constdouble _rowHeight =75;
/// The external padding around each row of the pseudo-table.constdouble _rowPadding =5;
/// The internal padding for each cell of the pseudo-table.constdouble _cellPadding =10;
/// The external padding around the widgets in the visibility report section.constdouble _reportPadding =5;
/// The height of the visibility report.constdouble _reportHeight =200;
/// The [Key] to the main [ListView] widget.const mainListKey =Key('MainList');
/// Returns the [Key] to the [VisibilityDetector] widget in each cell of the/// pseudo-table.KeycellKey(int row) =>Key('Cell-$row');
/// A callback to be invoked by the [VisibilityDetector.onVisibilityChanged]/// callback. We use the extra level of indirection to allow widget tests to/// reuse this demo app with a different callback.final visibilityListeners =<voidFunction(int row, VisibilityInfo info)>[];
voidmain() =>runApp(constVisibilityDetectorDemo());
/// The root widget for the demo app.classVisibilityDetectorDemoextendsStatelessWidget {
constVisibilityDetectorDemo({Key key}) :super(key: key);
@overrideWidgetbuild(BuildContext context) {
returnMaterialApp(
title: title,
theme:ThemeData(primarySwatch:Colors.blue),
home:constVisibilityDetectorDemoPage(),
);
}
}
/// The main page [VisibilityDetectorDemo].classVisibilityDetectorDemoPageextendsStatefulWidget {
constVisibilityDetectorDemoPage({Key key}) :super(key: key);
@overrideVisibilityDetectorDemoPageStatecreateState() =>VisibilityDetectorDemoPageState();
}
classVisibilityDetectorDemoPageStateextendsState<VisibilityDetectorDemoPage> {
/// Whether the pseudo-table should be shown.bool _tableShown =true;
/// Toggles the visibility of the pseudo-table of [VisibilityDetector] /// widgets.void_toggleTable() {
setState(() {
_tableShown =!_tableShown;
});
}
@overrideWidgetbuild(BuildContext context) {
// Our pseudo-table of [VisibilityDetector] widgets. We want to scroll both// vertically and horizontally, so we'll implement it as a [ListView] of// [ListView]s.final table =!_tableShown
?null:CustomScrollView(
key: mainListKey,
slivers: [
...List.generate(
100,
(rowIndex) =>DemoPageCell(
rowIndex: rowIndex,
))
],
);
returnScaffold(
appBar:AppBar(title:constText(title)),
floatingActionButton:FloatingActionButton(
shape:constBorder(),
onPressed: _toggleTable,
child: _tableShown ?constText('Hide') :constText('Show'),
),
body:Column(
children:<Widget>[
_tableShown ?Expanded(child: table) :constSpacer(),
constVisibilityReport(title:'Visibility'),
],
),
);
}
}
/// An individual cell for the pseudo-table of [VisibilityDetector] widgets.classDemoPageCellextendsStatelessWidget {
DemoPageCell({Key key, this.rowIndex})
: _cellName ='Item $rowIndex',
_backgroundColor = ((rowIndex) %2==0)
?Colors.pink[200]
:Colors.yellow[200],
super(key: key);
finalint rowIndex;
/// The text to show for the cell.finalString _cellName;
finalColor _backgroundColor;
/// [VisibilityDetector] callback for when the visibility of the widget /// changes. Triggers the [visibilityListeners] callbacks.void_handleVisibilityChanged(VisibilityInfo info) {
for (final listener in visibilityListeners) {
listener(rowIndex, info);
}
}
@overrideWidgetbuild(BuildContext context) {
returnSliverVisibilityDetector(
key:cellKey(rowIndex),
onVisibilityChanged: _handleVisibilityChanged,
sliver:SliverToBoxAdapter(
child:Container(
height: _rowHeight,
decoration:BoxDecoration(color: _backgroundColor),
padding:constEdgeInsets.all(_cellPadding),
alignment:Alignment.center,
child:FittedBox(
fit:BoxFit.scaleDown,
child:Text(_cellName, style:Theme.of(context).textTheme.headline4),
),
),
),
);
}
}
/// A widget that lists the reported visibility percentages of the/// [VisibilityDetector] widgets on the page.classVisibilityReportextendsStatelessWidget {
constVisibilityReport({Key key, this.title}) :super(key: key);
/// The text to use for the heading of the report.finalString title;
@overrideWidgetbuild(BuildContext context) {
final headingTextStyle =Theme.of(context).textTheme.headline6.copyWith(color:Colors.white);
final heading =Container(
padding:constEdgeInsets.all(_reportPadding),
alignment:Alignment.centerLeft,
decoration:constBoxDecoration(color:Colors.black),
child:Text(title, style: headingTextStyle),
);
final grid =Container(
padding:constEdgeInsets.all(_reportPadding),
decoration:BoxDecoration(color:Colors.grey[300]),
child:constSizedBox(
height: _reportHeight,
child:VisibilityReportGrid(),
),
);
returnColumn(children:<Widget>[heading, grid]);
}
}
/// The portion of [VisibilityReport] that shows data.classVisibilityReportGridextendsStatefulWidget {
constVisibilityReportGrid({Key key}) :super(key: key);
@overrideVisibilityReportGridStatecreateState() =>VisibilityReportGridState();
}
classVisibilityReportGridStateextendsState<VisibilityReportGrid> {
/// Maps [row, column] indices to the visibility percentage of the /// corresponding [VisibilityDetector] widget.final _visibilities =SplayTreeMap<int, double>();
/// The [Text] widgets used to fill our [GridView].List<Text> _reportItems;
/// See [State.initState]. Adds a callback to [visibilityListeners] to update /// the visibility report with the widget's visibility.@overridevoidinitState() {
super.initState();
visibilityListeners.add(_update);
assert(visibilityListeners.contains(_update));
}
@overridevoiddispose() {
visibilityListeners.remove(_update);
super.dispose();
}
/// Callback added to [visibilityListeners] to update the state.void_update(int row, VisibilityInfo info) {
setState(() {
if (info.visibleFraction ==0) {
_visibilities.remove(row);
} else {
print(info.size);
_visibilities[row] = info.visibleFraction;
}
// Invalidate `_reportItems` so that we regenerate it lazily.
_reportItems =null;
});
}
/// Populates [_reportItems].List<Text> _generateReportItems() {
final entries = _visibilities.entries;
final items =<Text>[];
for (final i in entries) {
final visiblePercentage = (i.value *100).toStringAsFixed(1);
items.add(Text('${i.key}: $visiblePercentage%'));
}
// It's easier to read cells down than across, so sort by columns instead of// by rows.final tailIndex = items.length - items.length ~/3;
final midIndex = tailIndex - tailIndex ~/2;
final head = items.getRange(0, midIndex);
final mid = items.getRange(midIndex, tailIndex);
final tail = items.getRange(tailIndex, items.length);
returncollate([head, mid, tail]).toList(growable:false);
}
@overrideWidgetbuild(BuildContext context) {
_reportItems ??=_generateReportItems();
returnGridView.count(
crossAxisCount:3,
childAspectRatio:8,
padding:constEdgeInsets.all(5),
children: _reportItems,
);
}
}
/// Returns an [Iterable] containing the nth element (if it exists) of every/// [Iterable] in `iterables` in sequence.////// Unlike [zip](https://pub.dev/documentation/quiver/latest/quiver.iterables/zip.html),/// returns a single sequence and continues until *all* [Iterable]s are/// exhausted.////// For example, `collate([[1, 4, 7], [2, 5, 8, 9], [3, 6]])` would return a/// sequence `1, 2, 3, 4, 5, 6, 7, 8, 9`.@visibleForTestingIterable<T> collate<T>(Iterable<Iterable<T>> iterables) sync* {
assert(iterables !=null);
final iterators = [for (final iterable in iterables) iterable.iterator];
if (iterators.isEmpty) {
return;
}
// ignore: literal_only_boolean_expressions, https://github.com/dart-lang/linter/issues/453while (true) {
var exhaustedCount =0;
for (final i in iterators) {
if (i.moveNext()) {
yield i.current;
continue;
}
exhaustedCount +=1;
if (exhaustedCount == iterators.length) {
// All iterators are at their ends.return;
}
}
}
}
The text was updated successfully, but these errors were encountered:
I'm not very familiar with slivers, so to be clear: is the issue about using, for example, a SliverList as the child of a VisibilityDetector, where the VisibilityDetector would report the visibility of the SliverList itself (which likely always would be 100% visible) and not of the content within the SliverList?
The idea was to report the content of the sliver. It might not look useful for a SliverList, but there are many more sliver with smaller size. My code was not working as expected when it reached the bottom boundary. I decided to use something different, so I won't be working on this more.
I feel like for the case of slivers, either the SliverList/SliverGrid/etc. should already know how much of its content is visible, or child widgets within them should be using VisibilityDetector instead. (Again, I'm not super familiar with slivers, so I might be totally wrong about use cases.)
Anyway, if you later decide that you want this after all with a clear and compelling use case, feel free to create a pull request.
VisibilityDetector
usesRenderProxy
, and can not be used to detect when a sliver appears/dissapears.It would be amazing to have a SliverVisibilityDetector.
The only important change for a SliverVisibilityDetector would be to use RenderSliverProxy and calculate the widgetSize with the geometry.scrollExtent. Right now RenderVisibilityDetector uses semanticBounds, that in RenderSliver uses geometry.paintExtent and doesn't correspond with the full size of the widget and only the part that is being painted.
Let me know if you would be interested to implement this or accept a PR. Otherwise I will make a package so it can be available to other developers
Full Example with SliverVisibilityDetector
The text was updated successfully, but these errors were encountered: