Skip to content

Commit db7aefd

Browse files
authored
[scrollable_positioned_list] Scroll offset controller (#474)
* Start scroll offset controller * Add scroll offset controller test * Add tests for stop scroll part way * Add tests for stop offset scroll part way * Clean up test and docs, add to examples * Fix analysis errors * Fix formatting * Fix formatting * Fix formatting
1 parent 2f15431 commit db7aefd

File tree

5 files changed

+345
-18
lines changed

5 files changed

+345
-18
lines changed

packages/scrollable_positioned_list/README.md

+10
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ A `ScrollablePositionedList` can be created with:
1515

1616
```dart
1717
final ItemScrollController itemScrollController = ItemScrollController();
18+
final ScrollOffsetController scrollOffsetController = ScrollOffsetController();
1819
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
1920
final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create()
2021
2122
ScrollablePositionedList.builder(
2223
itemCount: 500,
2324
itemBuilder: (context, index) => Text('Item $index'),
2425
itemScrollController: itemScrollController,
26+
scrollOffsetController: scrollOffsetController,
2527
itemPositionsListener: itemPositionsListener,
2628
scrollOffsetListener: scrollOffsetListener,
2729
);
@@ -48,6 +50,8 @@ One can monitor what items are visible on screen with:
4850
itemPositionsListener.itemPositions.addListener(() => ...);
4951
```
5052

53+
### Experimental APIs (subject to bugs and changes)
54+
5155
Changes in scroll position can be monitored with:
5256

5357
```dart
@@ -57,6 +61,12 @@ scrollOffsetListener.changes.listen((event) => ...)
5761
see `ScrollSum` in [this test](test/scroll_offset_listener_test.dart) for an example of how the current offset can be
5862
calculated from the stream of scroll change deltas. This feature is new and experimental.
5963

64+
Changes in scroll position in pixels, relative to the current scroll position, can be made with:
65+
66+
```dart
67+
scrollOffsetController.animateScroll(offset: 100, duration: Duration(seconds: 1));
68+
```
69+
6070
A full example can be found in the example folder.
6171

6272
--------------------------------------------------------------------------------

packages/scrollable_positioned_list/example/lib/main.dart

+54-16
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ class _ScrollablePositionedListPageState
5555
/// Controller to scroll or jump to a particular item.
5656
final ItemScrollController itemScrollController = ItemScrollController();
5757

58+
/// Controller to scroll a certain number of pixels relative to the current
59+
/// scroll offset.
60+
final ScrollOffsetController scrollOffsetController =
61+
ScrollOffsetController();
62+
5863
/// Listener that reports the position of items when the list is scrolled.
5964
final ItemPositionsListener itemPositionsListener =
6065
ItemPositionsListener.create();
@@ -89,10 +94,12 @@ class _ScrollablePositionedListPageState
8994
),
9095
positionsView,
9196
Row(
97+
mainAxisSize: MainAxisSize.min,
9298
children: <Widget>[
9399
Column(
94100
children: <Widget>[
95101
scrollControlButtons,
102+
scrollOffsetControlButtons,
96103
const SizedBox(height: 10),
97104
jumpControlButtons,
98105
alignmentControl,
@@ -130,6 +137,7 @@ class _ScrollablePositionedListPageState
130137
itemBuilder: (context, index) => item(index, orientation),
131138
itemScrollController: itemScrollController,
132139
itemPositionsListener: itemPositionsListener,
140+
scrollOffsetController: scrollOffsetController,
133141
reverse: reversed,
134142
scrollDirection: orientation == Orientation.portrait
135143
? Axis.vertical
@@ -181,12 +189,24 @@ class _ScrollablePositionedListPageState
181189
Widget get scrollControlButtons => Row(
182190
children: <Widget>[
183191
const Text('scroll to'),
184-
scrollButton(0),
185-
scrollButton(5),
186-
scrollButton(10),
187-
scrollButton(100),
188-
scrollButton(1000),
189-
scrollButton(5000),
192+
scrollItemButton(0),
193+
scrollItemButton(5),
194+
scrollItemButton(10),
195+
scrollItemButton(100),
196+
scrollItemButton(1000),
197+
scrollItemButton(5000),
198+
],
199+
);
200+
201+
Widget get scrollOffsetControlButtons => Row(
202+
children: <Widget>[
203+
const Text('scroll by'),
204+
scrollOffsetButton(-1000),
205+
scrollOffsetButton(-100),
206+
scrollOffsetButton(-10),
207+
scrollOffsetButton(10),
208+
scrollOffsetButton(100),
209+
scrollOffsetButton(1000),
190210
],
191211
);
192212

@@ -202,26 +222,41 @@ class _ScrollablePositionedListPageState
202222
],
203223
);
204224

205-
final _scrollButtonStyle = ButtonStyle(
206-
padding: MaterialStateProperty.all(
207-
const EdgeInsets.symmetric(horizontal: 20, vertical: 0),
208-
),
209-
minimumSize: MaterialStateProperty.all(Size.zero),
210-
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
211-
);
225+
ButtonStyle _scrollButtonStyle({required double horizonalPadding}) =>
226+
ButtonStyle(
227+
padding: MaterialStateProperty.all(
228+
EdgeInsets.symmetric(horizontal: horizonalPadding, vertical: 0),
229+
),
230+
minimumSize: MaterialStateProperty.all(Size.zero),
231+
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
232+
);
212233

213-
Widget scrollButton(int value) => TextButton(
234+
Widget scrollItemButton(int value) => TextButton(
214235
key: ValueKey<String>('Scroll$value'),
215236
onPressed: () => scrollTo(value),
216237
child: Text('$value'),
217-
style: _scrollButtonStyle,
238+
style: _scrollButtonStyle(horizonalPadding: 20),
239+
);
240+
241+
Widget scrollOffsetButton(int value) => TextButton(
242+
key: ValueKey<String>('Scroll$value'),
243+
onPressed: () => scrollBy(value.toDouble()),
244+
child: Text('$value'),
245+
style: _scrollButtonStyle(horizonalPadding: 10),
246+
);
247+
248+
Widget scrollPixelButton(int value) => TextButton(
249+
key: ValueKey<String>('Scroll$value'),
250+
onPressed: () => scrollTo(value),
251+
child: Text('$value'),
252+
style: _scrollButtonStyle(horizonalPadding: 20),
218253
);
219254

220255
Widget jumpButton(int value) => TextButton(
221256
key: ValueKey<String>('Jump$value'),
222257
onPressed: () => jumpTo(value),
223258
child: Text('$value'),
224-
style: _scrollButtonStyle,
259+
style: _scrollButtonStyle(horizonalPadding: 20),
225260
);
226261

227262
void scrollTo(int index) => itemScrollController.scrollTo(
@@ -230,6 +265,9 @@ class _ScrollablePositionedListPageState
230265
curve: Curves.easeInOutCubic,
231266
alignment: alignment);
232267

268+
void scrollBy(double offset) => scrollOffsetController.animateScroll(
269+
offset: offset, duration: scrollDuration, curve: Curves.easeInOutCubic);
270+
233271
void jumpTo(int index) =>
234272
itemScrollController.jumpTo(index: index, alignment: alignment);
235273

packages/scrollable_positioned_list/lib/src/scroll_offset_listener.dart

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import 'dart:async';
33
import 'scroll_offset_notifier.dart';
44

55
/// Provides an affordance for listening to scroll offset changes.
6+
///
7+
/// This is an experimental API and is subject to change.
8+
/// Behavior may be ill-defined in some cases. Please file bugs.
69
abstract class ScrollOffsetListener {
710
/// Stream of scroll offset deltas.
811
Stream<double> get changes;

packages/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart

+49-2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import 'dart:math';
88
import 'package:collection/collection.dart' show IterableExtension;
99
import 'package:flutter/scheduler.dart';
1010
import 'package:flutter/widgets.dart';
11+
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
1112

12-
import 'item_positions_listener.dart';
1313
import 'item_positions_notifier.dart';
1414
import 'positioned_list.dart';
1515
import 'post_mount_callback.dart';
16-
import 'scroll_offset_listener.dart';
1716
import 'scroll_offset_notifier.dart';
1817

1918
/// Number of screens to scroll when scrolling a long distance.
@@ -45,6 +44,7 @@ class ScrollablePositionedList extends StatefulWidget {
4544
this.itemScrollController,
4645
this.shrinkWrap = false,
4746
ItemPositionsListener? itemPositionsListener,
47+
this.scrollOffsetController,
4848
ScrollOffsetListener? scrollOffsetListener,
4949
this.initialScrollIndex = 0,
5050
this.initialAlignment = 0,
@@ -74,6 +74,7 @@ class ScrollablePositionedList extends StatefulWidget {
7474
this.shrinkWrap = false,
7575
this.itemScrollController,
7676
ItemPositionsListener? itemPositionsListener,
77+
this.scrollOffsetController,
7778
ScrollOffsetListener? scrollOffsetListener,
7879
this.initialScrollIndex = 0,
7980
this.initialAlignment = 0,
@@ -110,6 +111,8 @@ class ScrollablePositionedList extends StatefulWidget {
110111
/// Notifier that reports the items laid out in the list after each frame.
111112
final ItemPositionsNotifier? itemPositionsNotifier;
112113

114+
final ScrollOffsetController? scrollOffsetController;
115+
113116
/// Notifier that reports the changes to the scroll offset.
114117
final ScrollOffsetNotifier? scrollOffsetNotifier;
115118

@@ -267,6 +270,41 @@ class ItemScrollController {
267270
}
268271
}
269272

273+
/// Controller to scroll a certain number of pixels relative to the current
274+
/// scroll offset.
275+
///
276+
/// Scrolls [offset] pixels relative to the current scroll offset. [offset] can
277+
/// be positive or negative.
278+
///
279+
/// This is an experimental API and is subject to change.
280+
/// Behavior may be ill-defined in some cases. Please file bugs.
281+
class ScrollOffsetController {
282+
Future<void> animateScroll(
283+
{required double offset,
284+
required Duration duration,
285+
Curve curve = Curves.linear}) async {
286+
final currentPosition =
287+
_scrollableListState!.primary.scrollController.offset;
288+
final newPosition = currentPosition + offset;
289+
await _scrollableListState!.primary.scrollController.animateTo(
290+
newPosition,
291+
duration: duration,
292+
curve: curve,
293+
);
294+
}
295+
296+
_ScrollablePositionedListState? _scrollableListState;
297+
298+
void _attach(_ScrollablePositionedListState scrollableListState) {
299+
assert(_scrollableListState == null);
300+
_scrollableListState = scrollableListState;
301+
}
302+
303+
void _detach() {
304+
_scrollableListState = null;
305+
}
306+
}
307+
270308
class _ScrollablePositionedListState extends State<ScrollablePositionedList>
271309
with TickerProviderStateMixin {
272310
/// Details for the primary (active) [ListView].
@@ -297,6 +335,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
297335
primary.target = widget.itemCount - 1;
298336
}
299337
widget.itemScrollController?._attach(this);
338+
widget.scrollOffsetController?._attach(this);
300339
primary.itemPositionsNotifier.itemPositions.addListener(_updatePositions);
301340
secondary.itemPositionsNotifier.itemPositions.addListener(_updatePositions);
302341
primary.scrollController.addListener(() {
@@ -310,9 +349,17 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
310349
});
311350
}
312351

352+
@override
353+
void activate() {
354+
super.activate();
355+
widget.itemScrollController?._attach(this);
356+
widget.scrollOffsetController?._attach(this);
357+
}
358+
313359
@override
314360
void deactivate() {
315361
widget.itemScrollController?._detach();
362+
widget.scrollOffsetController?._detach();
316363
super.deactivate();
317364
}
318365

0 commit comments

Comments
 (0)