Unverified Commit b4777c35 authored by Casey Rogers's avatar Casey Rogers Committed by GitHub

Make `DraggableScrollableController` a `ChangeNotifier` (#96089)

parent f01556ab
......@@ -44,7 +44,14 @@ typedef ScrollableWidgetBuilder = Widget Function(
///
/// The controller's methods cannot be used until after the controller has been
/// passed into a [DraggableScrollableSheet] and the sheet has run initState.
class DraggableScrollableController {
///
/// A [DraggableScrollableController] is a [Listenable]. It notifies its
/// listeners whenever an attached sheet changes sizes. It does not notify its
/// listeners when a sheet is first attached or when an attached sheet's
/// parameters change without affecting the sheet's current size. It does not
/// fire when [pixels] changes without [size] changing. For example, if the
/// constraints provided to an attached sheet change.
class DraggableScrollableController extends ChangeNotifier {
_DraggableScrollableSheetScrollController? _attachedController;
/// Get the current size (as a fraction of the parent height) of the attached sheet.
......@@ -160,9 +167,23 @@ class DraggableScrollableController {
void _attach(_DraggableScrollableSheetScrollController scrollController) {
assert(_attachedController == null, 'Draggable scrollable controller is already attached to a sheet.');
_attachedController = scrollController;
_attachedController!.extent._currentSize.addListener(notifyListeners);
}
void _onExtentReplaced(_DraggableSheetExtent previousExtent) {
// When the extent has been replaced, the old extent is already disposed and
// the controller will point to a new extent. We have to add our listener to
// the new extent.
_attachedController!.extent._currentSize.addListener(notifyListeners);
if (previousExtent.currentSize != _attachedController!.extent.currentSize) {
// The listener won't fire for a change in size between two extent
// objects so we have to fire it manually here.
notifyListeners();
}
}
void _detach() {
_attachedController?.extent._currentSize.removeListener(notifyListeners);
_attachedController = null;
}
}
......@@ -635,6 +656,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
}
void _replaceExtent() {
final _DraggableSheetExtent previousExtent = _extent;
_extent.dispose();
_extent = _extent.copyWith(
minSize: widget.minChildSize,
......@@ -647,6 +669,9 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
// Modify the existing scroll controller instead of replacing it so that
// developers listening to the controller do not have to rebuild their listeners.
_scrollController.extent = _extent;
// If an external facing controller was provided, let it know that the
// extent has been replaced.
widget.controller?._onExtentReplaced(previousExtent);
if (widget.snap) {
// Trigger a snap in case snap or snapSizes has changed. We put this in a
// post frame callback so that `build` can update `_extent.availablePixels`
......
......@@ -976,6 +976,147 @@ void main() {
expect(tester.takeException(), isAssertionError);
});
testWidgets('Can listen for changes in sheet size', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final List<double> loggedSizes = <double>[];
final DraggableScrollableController controller = DraggableScrollableController();
controller.addListener(() {
loggedSizes.add(controller.size);
});
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester
.getSize(find.byKey(stackKey))
.height;
// The initial size shouldn't be logged because no change has occurred yet.
expect(loggedSizes.isEmpty, true);
await tester.drag(find.text('Item 1'), Offset(0, .1 * screenHeight), touchSlopY: 0);
await tester.pumpAndSettle();
expect(loggedSizes, <double>[.4].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
await tester.timedDrag(find.text('Item 1'), Offset(0, -.1 * screenHeight), const Duration(seconds: 1), frequency: 2);
await tester.pumpAndSettle();
expect(loggedSizes, <double>[.45, .5].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
controller.jumpTo(.6);
await tester.pumpAndSettle();
expect(loggedSizes, <double>[.6].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
controller.animateTo(1, duration: const Duration(milliseconds: 400), curve: Curves.linear);
await tester.pumpAndSettle();
expect(loggedSizes, <double>[.7, .8, .9, 1].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
DraggableScrollableActuator.reset(tester.element(find.byKey(containerKey)));
await tester.pumpAndSettle();
expect(loggedSizes, <double>[.5].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
});
testWidgets('Listener does not fire on parameter change and persists after change', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final List<double> loggedSizes = <double>[];
final DraggableScrollableController controller = DraggableScrollableController();
controller.addListener(() {
loggedSizes.add(controller.size);
});
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester
.getSize(find.byKey(stackKey))
.height;
expect(loggedSizes.isEmpty, true);
await tester.drag(find.text('Item 1'), Offset(0, .1 * screenHeight), touchSlopY: 0);
await tester.pumpAndSettle();
expect(loggedSizes, <double>[.4].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
// Update a parameter without forcing a change in the current size.
await tester.pumpWidget(_boilerplate(
null,
minChildSize: .1,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
expect(loggedSizes.isEmpty, true);
await tester.drag(find.text('Item 1'), Offset(0, .1 * screenHeight), touchSlopY: 0);
await tester.pumpAndSettle();
expect(loggedSizes, <double>[.3].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
});
testWidgets('Listener fires if a parameter change forces a change in size', (WidgetTester tester) async {
const Key stackKey = ValueKey<String>('stack');
const Key containerKey = ValueKey<String>('container');
final List<double> loggedSizes = <double>[];
final DraggableScrollableController controller = DraggableScrollableController();
controller.addListener(() {
loggedSizes.add(controller.size);
});
await tester.pumpWidget(_boilerplate(
null,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
await tester.pumpAndSettle();
final double screenHeight = tester
.getSize(find.byKey(stackKey))
.height;
expect(loggedSizes.isEmpty, true);
// Set a new `initialChildSize` which will trigger a size change because we
// haven't moved away initial size yet.
await tester.pumpWidget(_boilerplate(
null,
initialChildSize: .6,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
expect(loggedSizes, <double>[.6].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
// Move away from initial child size.
await tester.drag(find.text('Item 1'), Offset(0, .3 * screenHeight), touchSlopY: 0);
await tester.pumpAndSettle();
expect(loggedSizes, <double>[.3].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
// Set a `minChildSize` greater than the current size.
await tester.pumpWidget(_boilerplate(
null,
minChildSize: .4,
controller: controller,
stackKey: stackKey,
containerKey: containerKey,
));
expect(loggedSizes, <double>[.4].map((double v) => closeTo(v, precisionErrorTolerance)));
loggedSizes.clear();
});
testWidgets('Invalid controller interactions throw assertion errors', (WidgetTester tester) async {
final DraggableScrollableController controller = DraggableScrollableController();
// Can't use a controller before attaching it.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment