Skip to content

Commit

Permalink
support different observers
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonWang9610 committed Mar 15, 2023
1 parent 20def04 commit 21b257c
Show file tree
Hide file tree
Showing 24 changed files with 511 additions and 1,306 deletions.
259 changes: 110 additions & 149 deletions README.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions example/lib/example/custom_view_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ class _CustomViewExampleState extends State<CustomViewExample> {

final ScrollController _controller = ScrollController();

late final keepAlive = MultiChildSliverObserver(itemCount: _itemCount);
late final grid = MultiChildSliverObserver(itemCount: _itemCount);
late final list = MultiChildSliverObserver(itemCount: _itemCount);
late final appbar = SingleChildSliverObserver();
late final keepAlive = ScrollObserver.sliverMulti(itemCount: _itemCount);
late final grid = ScrollObserver.sliverMulti(itemCount: _itemCount);
late final list = ScrollObserver.sliverMulti(itemCount: _itemCount);
late final appbar = ScrollObserver.sliverSingle();

@override
void dispose() {
Expand Down
2 changes: 1 addition & 1 deletion example/lib/example/grid_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class _PositionedGridExampleState extends State<PositionedGridExample> {
int _itemCount = 30;

final ScrollController _controller = ScrollController();
late final grid = MultiChildSliverObserver(itemCount: _itemCount);
late final grid = ScrollObserver.sliverMulti(itemCount: _itemCount);

final String observerKey = "grid";
@override
Expand Down
32 changes: 19 additions & 13 deletions example/lib/example/list_wheel_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ListWheelExample extends StatefulWidget {
class _ListWheelExampleState extends State<ListWheelExample> {
final ScrollController _controller = ScrollController();

final MultiChildBoxObserver _observer = MultiChildBoxObserver(
final _observer = ScrollObserver.boxMulti(
axis: Axis.vertical,
itemCount: 30,
);
Expand Down Expand Up @@ -47,18 +47,24 @@ class _ListWheelExampleState extends State<ListWheelExample> {
},
),
Expanded(
child: ListWheelScrollView(
controller: _controller,
itemExtent: 100,
children: [
for (int i = 0; i < 30; i++)
ObserverProxy(
observer: _observer,
child: Center(
child: Text("List Wheel $i"),
),
)
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: ListWheelScrollView(
controller: _controller,
itemExtent: 100,
children: [
for (int i = 0; i < 30; i++)
ObserverProxy(
observer: _observer,
child: DecoratedBox(
decoration: BoxDecoration(border: Border.all()),
child: Center(
child: Text("List Wheel $i"),
),
),
)
],
),
),
),
],
Expand Down
25 changes: 19 additions & 6 deletions example/lib/example/official_list_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ class OfficialListExample extends StatefulWidget {
}

class _OfficialListExampleState extends State<OfficialListExample> {
int _itemCount = 30;
int _itemCount = 1000;

final ScrollController _controller = ScrollController();
// late final ScrollObserver _observer =
// ScrollObserver.multiChild(itemCount: _itemCount);

late final SliverScrollObserver _observer =
MultiChildSliverObserver(itemCount: _itemCount);

Expand All @@ -29,6 +28,20 @@ class _OfficialListExampleState extends State<OfficialListExample> {

@override
Widget build(BuildContext context) {
ListView.builder(
controller: _controller,
itemBuilder: (context, index) => SliverObserverProxy(
observer: _observer,
child: ListTile(
key: ValueKey<int>(index),
leading: const CircleAvatar(
child: Text("L"),
),
title: Text("Positioned List Example $index"),
),
),
itemCount: _itemCount,
);
return Scaffold(
appBar: AppBar(
centerTitle: true,
Expand Down Expand Up @@ -68,7 +81,7 @@ class _OfficialListExampleState extends State<OfficialListExample> {
_observer.animateToIndex(
index,
position: _controller.position,
duration: const Duration(milliseconds: 200),
duration: const Duration(milliseconds: 1000),
curve: Curves.fastLinearToSlowEaseIn,
);
},
Expand Down Expand Up @@ -130,11 +143,11 @@ class OfficialSeparatedListExample extends StatefulWidget {

class _OfficialSeparatedListExampleState
extends State<OfficialSeparatedListExample> {
int _itemCount = 30;
int _itemCount = 1000;

final ScrollController _controller = ScrollController();

late final SliverScrollObserver _observer = MultiChildSliverObserver(
late final _observer = ScrollObserver.sliverMulti(
itemCount: _computeActualChildCount(_itemCount),
)
..targetToRenderIndex = _toRenderIndex
Expand Down
7 changes: 3 additions & 4 deletions example/lib/example/reorderable_list_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ class _OfficialReorderableListExampleState
extends State<OfficialReorderableListExample> {
final ScrollController _controller = ScrollController();

late final SliverScrollObserver _observer =
MultiChildSliverObserver(itemCount: _items.length)
..targetToRenderIndex = _toRenderIndex
..renderToTargetIndex = _toTargetIndex;
late final _observer = ScrollObserver.sliverMulti(itemCount: _items.length)
..targetToRenderIndex = _toRenderIndex
..renderToTargetIndex = _toTargetIndex;

late final List<int> _items = List.generate(
30,
Expand Down
28 changes: 25 additions & 3 deletions example/lib/example/single_child_scroll_view_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,37 @@ class SingleChildScrollExample extends StatefulWidget {
class _SingleChildScrollExampleState extends State<SingleChildScrollExample> {
final ScrollController _controller = ScrollController();

late final MultiChildBoxObserver _observer = MultiChildBoxObserver(
late final _observer = ScrollObserver.boxMulti(
axis: _axis,
itemCount: 30,
);

Axis _axis = Axis.horizontal;
Axis _axis = Axis.vertical;

@override
Widget build(BuildContext context) {
SingleChildScrollView(
controller: _controller,
scrollDirection: _axis,
child: Column(
children: [
for (int i = 0; i < 30; i++)
ObserverProxy(
observer: _observer,
child: DecoratedBox(
decoration: BoxDecoration(border: Border.all()),
child: SizedBox(
height: 100,
width: 100,
child: Center(
child: Text("Column item $i"),
),
),
),
),
],
),
);
return Scaffold(
appBar: AppBar(
centerTitle: true,
Expand Down Expand Up @@ -53,7 +75,7 @@ class _SingleChildScrollExampleState extends State<SingleChildScrollExample> {
child: SingleChildScrollView(
controller: _controller,
scrollDirection: _axis,
child: Row(
child: Column(
children: [
for (int i = 0; i < 30; i++)
BoxObserverProxy(
Expand Down
1 change: 1 addition & 0 deletions lib/positioned_scroll_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ library positioned_scroll_observer;

export 'src/widgets/widgets.dart';
export 'src/observer/observer.dart';
export 'src/observer_factory.dart';
15 changes: 8 additions & 7 deletions lib/src/observer/box_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,16 @@ class MultiChildBoxObserver extends BoxScrollObserver<MultiChildRenderBox>
);

if (shouldDoFinishLayout && isObserving) {
print("$runtimeType: doFinishLayout");
shouldDoFinishLayout = false;

RenderBox? child = (renderObject as MultiChildRenderBox).firstChild;

int? first;
int? last;

int count = 0;
double totalExtent = 0;

int? first;
int? last;

while (child != null) {
final parentData =
child.parentData! as ContainerBoxParentData<RenderBox>;
Expand All @@ -145,7 +144,8 @@ class MultiChildBoxObserver extends BoxScrollObserver<MultiChildRenderBox>
final item = ItemScrollExtent.fromBoxData(index, parentData, axis);

items[item.index] = item;
totalExtent += item.mainAxisOffset;

totalExtent += getMainAxisExtent(item.index);

first = lessFirst(first, item.index);
last = greaterLast(last, item.index);
Expand Down Expand Up @@ -187,10 +187,11 @@ class MultiChildBoxObserver extends BoxScrollObserver<MultiChildRenderBox>
}
}

print("$runtimeType: $onstageItems");
debugPrint("$runtimeType: $onstageItems");
}
}

// todo: waiting testing
class SingleChildBoxObserver extends BoxScrollObserver<SingleChildRenderBox>
with SingleChildEstimation {
SingleChildBoxObserver({required super.axis});
Expand All @@ -215,7 +216,7 @@ class SingleChildBoxObserver extends BoxScrollObserver<SingleChildRenderBox>
ParentData? parentData,
}) {
super.onLayout(value, size: size, parentData: parentData);
size = size;
this.size = size;
}

@override
Expand Down
50 changes: 40 additions & 10 deletions lib/src/observer/item_estimation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import 'package:positioned_scroll_observer/src/observer/scroll_extent.dart';

import 'onstage_strategy.dart';

/// Used for observers whose [hasMultiChild] is true.
/// Such observers need to observe multi children for a [RenderObject],
/// and [estimateScrollOffset] when developers want to scroll to a specific index.
mixin MultiChildEstimation<T extends RenderObject>
on LayoutObserver<T>, ObserverScrollInterface {
final Map<int, ItemScrollExtent> _items = {};
final Map<int, Size> _sizes = {};

double _estimatedAveragePageGap = 0;
/// the estimated average extent for each item.
/// updated when [doFinishLayout] actually observes some items.
/// If the item has a fixed extent, it should be equal to the fixed item extent after serval jump/animate.
double _averageExtentForEachIndex = 0;

int? _first;
int? _last;
Expand All @@ -22,14 +28,24 @@ mixin MultiChildEstimation<T extends RenderObject>
bool get firstLayoutFinished =>
_first != null && _last != null && super.firstLayoutFinished;

/// record those children that have been laid out
void updateRange(int? first, int? last) {
_first = first;
_last = last;
}

/// the [Size] of laid out children, the key may be its index for [SliverScrollObserver],
/// or [ParentData.hashCode] for [BoxScrollObserver]
Map<int, Size> get sizes => _sizes;

/// the items whose [ItemScrollExtent] have been observed after [doFinishLayout]
Map<int, ItemScrollExtent> get items => _items;

/// estimate the scroll offset for [target] based on either its [ItemScrollExtent] or
/// [_averageExtentForEachIndex].
/// Use [_first] or [_last] to indicate if scroll up or scroll down.
/// The final estimation would be clamped between [ScrollExtent.min] and [ScrollExtent.max]
/// to avoid over scrolling.
@override
double estimateScrollOffset(
int target, {
Expand All @@ -46,15 +62,12 @@ mixin MultiChildEstimation<T extends RenderObject>
if (_items.containsKey(target)) {
estimated += getItemScrollExtent(target)!.mainAxisOffset;
} else {
/// avoid division by zero when estimating
final currentIndexGap = _last! - _first! > 0 ? _last! - _first! : 1;

if (target < _first!) {
estimated += getItemScrollExtent(_first!)!.mainAxisOffset +
(target - _first!) / currentIndexGap * _estimatedAveragePageGap;
(target - _first!) * _averageExtentForEachIndex;
} else if (target > _last!) {
estimated += getItemScrollExtent(_last!)!.mainAxisOffset +
(target - _last!) / currentIndexGap * _estimatedAveragePageGap;
(target - _last!) * _averageExtentForEachIndex;
} else {
assert(
false,
Expand All @@ -69,18 +82,29 @@ mixin MultiChildEstimation<T extends RenderObject>
return clampDouble(estimated, leadingEdge, scrollExtent.max);
}

/// average [_averageExtentForEachIndex] based on the current observed [totalExtent],
/// maybe we should have a better way to summarize the previous estimation: [_averageExtentForEachIndex]
/// and the current exact extent: [totalExtent]
void updateEstimation(double totalExtent, int count) {
final average = count == 0 ? totalExtent : totalExtent / count;

_estimatedAveragePageGap = (average + _estimatedAveragePageGap) / 2;
if (_averageExtentForEachIndex == 0) {
_averageExtentForEachIndex = average;
} else {
_averageExtentForEachIndex = (average + _averageExtentForEachIndex) / 2;
}
}

@override
ItemScrollExtent? getItemScrollExtent(int index) => items[index];

/// Different observers may override [getItemSize] base on how they store sizes of items.
/// e.g., [BoxScrollObserver] would override this method since [sizes]'s key is [ParentData.hashCode]
/// instead of its [index]
@override
Size? getItemSize(int index) => sizes[index];

///
@override
int normalizeIndex(int index) {
if (itemCount == null) {
Expand All @@ -105,7 +129,7 @@ mixin MultiChildEstimation<T extends RenderObject>
}
}

print("$runtimeType: $onstageItems");
debugPrint("$runtimeType: $onstageItems");
}

@override
Expand All @@ -115,10 +139,13 @@ mixin MultiChildEstimation<T extends RenderObject>
_sizes.clear();
_first = null;
_last = null;
_estimatedAveragePageGap = 0;
_averageExtentForEachIndex = 0;
}
}

/// Used for observers whose [hasMultiChild] is false.
/// [itemCount] for such observers is always 1, since it only has one child.
/// therefore, the setter of [itemCount] is always setting as 1.
mixin SingleChildEstimation<T extends RenderObject>
on LayoutObserver<T>, ObserverScrollInterface {
Size? _size;
Expand Down Expand Up @@ -146,6 +173,9 @@ mixin SingleChildEstimation<T extends RenderObject>
@override
Size? getItemSize(int index) => _size;

/// Since we would use [showInViewport] to display the [renderObject] on the screen,
/// therefore, we could guarantee the child of [renderObject] is also visible on the screen.
/// As a result, we should never adjust the position of the child.
@override
double estimateScrollOffset(
int target, {
Expand All @@ -159,7 +189,7 @@ mixin SingleChildEstimation<T extends RenderObject>
required ScrollExtent scrollExtent,
PredicatorStrategy strategy = PredicatorStrategy.tolerance,
}) {
print("$runtimeType: $renderVisible");
debugPrint("$runtimeType: $renderVisible");
}

@override
Expand Down
Loading

0 comments on commit 21b257c

Please sign in to comment.