Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[new feature]introduce ScrollMetricsNotification #85221

Merged
merged 2 commits into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
126 changes: 125 additions & 1 deletion packages/flutter/lib/src/widgets/scroll_position.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
Expand All @@ -10,6 +12,7 @@ import 'package:flutter/scheduler.dart';

import 'basic.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'page_storage.dart';
import 'scroll_activity.dart';
import 'scroll_context.dart';
Expand Down Expand Up @@ -508,6 +511,17 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
ScrollMetrics? _lastMetrics;
Axis? _lastAxis;

bool _isMetricsChanged() {
assert(haveDimensions);
final ScrollMetrics currentMetrics = copyWith();

return _lastMetrics == null ||
!(currentMetrics.extentBefore == _lastMetrics!.extentBefore
&& currentMetrics.extentInside == _lastMetrics!.extentInside
&& currentMetrics.extentAfter == _lastMetrics!.extentAfter
&& currentMetrics.axisDirection == _lastMetrics!.axisDirection);
}

@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
assert(minScrollExtent != null);
Expand Down Expand Up @@ -537,7 +551,14 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
_pendingDimensions = false;
}
assert(!_didChangeViewportDimensionOrReceiveCorrection, 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().');
_lastMetrics = copyWith();

if (_isMetricsChanged()) {
// It isn't safe to trigger the ScrollMetricsNotification if we are in
// the middle of rendering the frame, the developer is likely to schedule
// a new frame(build scheduled during frame is illegal).
scheduleMicrotask(didUpdateScrollMetrics);
_lastMetrics = copyWith();
}
return true;
}

Expand Down Expand Up @@ -898,6 +919,13 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
UserScrollNotification(metrics: copyWith(), context: context.notificationContext!, direction: direction).dispatch(context.notificationContext);
}

/// Dispatches a notification that the [ScrollMetrics] has changed.
void didUpdateScrollMetrics() {
assert(SchedulerBinding.instance!.schedulerPhase != SchedulerPhase.persistentCallbacks);
if (context.notificationContext != null)
ScrollMetricsNotification(metrics: copyWith(), context: context.notificationContext!).dispatch(context.notificationContext);
}

/// Provides a heuristic to determine if expensive frame-bound tasks should be
/// deferred.
///
Expand Down Expand Up @@ -942,3 +970,99 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
description.add('viewport: ${_viewportDimension?.toStringAsFixed(1)}');
}
}

/// A notification that a scrollable widget's [ScrollMetrics] have changed.
///
/// For example, when the content of a scrollable is altered, making it larger
/// or smaller, this notification will be dispatched. Similarly, if the size
/// of the window or parent changes, the scrollable can notify of these
/// changes in dimensions.
///
/// The above behaviors usually do not trigger [ScrollNotification] events,
/// so this is useful for listening to [ScrollMetrics] changes that are not
/// caused by the user scrolling.
///
/// {@tool dartpad --template=freeform}
/// This sample shows how a [ScrollMetricsNotification] is dispatched when
/// the `windowSize` is changed. Press the floating action button to increase
/// the scrollable window's size.
///
/// ```dart main
/// import 'package:flutter/material.dart';
///
/// void main() => runApp(const ScrollMetricsDemo());
///
/// class ScrollMetricsDemo extends StatefulWidget {
/// const ScrollMetricsDemo({Key? key}) : super(key: key);
///
/// @override
/// State<ScrollMetricsDemo> createState() => ScrollMetricsDemoState();
/// }
///
/// class ScrollMetricsDemoState extends State<ScrollMetricsDemo> {
/// double windowSize = 200.0;
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: Scaffold(
/// appBar: AppBar(
/// title: const Text('ScrollMetrics Demo'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// child: const Icon(Icons.add),
/// onPressed: () => setState(() {
/// windowSize += 10.0;
/// }),
/// ),
/// body: NotificationListener<ScrollMetricsNotification>(
/// onNotification: (ScrollMetricsNotification notification) {
/// ScaffoldMessenger.of(notification.context).showSnackBar(
/// const SnackBar(
/// content: Text('Scroll metrics changed!'),
/// ),
/// );
/// return false;
/// },
/// child: Scrollbar(
/// isAlwaysShown: true,
/// child: SizedBox(
/// height: windowSize,
/// width: double.infinity,
/// child: const SingleChildScrollView(
/// child: FlutterLogo(
/// size: 300.0,
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
class ScrollMetricsNotification extends LayoutChangedNotification with ViewportNotificationMixin {
/// Creates a notification that the scrollable widget's [ScrollMetrics] have
/// changed.
ScrollMetricsNotification({
required this.metrics,
required this.context,
});

/// Description of a scrollable widget's [ScrollMetrics].
final ScrollMetrics metrics;

/// The build context of the widget that fired this notification.
///
/// This can be used to find the scrollable widget's render objects to
/// determine the size of the viewport, for instance.
final BuildContext context;

@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$metrics');
}
}
58 changes: 58 additions & 0 deletions packages/flutter/test/widgets/scroll_notification_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,64 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('ScrollMetricsNotification test', (WidgetTester tester) async {
final List<LayoutChangedNotification> events = <LayoutChangedNotification>[];
Widget buildFrame(double height) {
return NotificationListener<LayoutChangedNotification>(
onNotification: (LayoutChangedNotification value) {
events.add(value);
return false;
},
child: SingleChildScrollView(
child: SizedBox(height: height),
),
);
}
await tester.pumpWidget(buildFrame(1200.0));
// Initial metrics notification.
expect(events.length, 1);
ScrollMetricsNotification event = events[0] as ScrollMetricsNotification;
expect(event.metrics.extentBefore, 0.0);
expect(event.metrics.extentInside, 600.0);
expect(event.metrics.extentAfter, 600.0);

events.clear();
await tester.pumpWidget(buildFrame(1000.0));
// Change the content dimensions will trigger a new event.
expect(events.length, 1);
event = events[0] as ScrollMetricsNotification;
expect(event.metrics.extentBefore, 0.0);
expect(event.metrics.extentInside, 600.0);
expect(event.metrics.extentAfter, 400.0);

events.clear();
final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
expect(events.length, 1);
// user scroll do not trigger the ScrollContentMetricsNotification.
expect(events[0] is ScrollStartNotification, true);

events.clear();
await gesture.moveBy(const Offset(-10.0, -10.0));
expect(events.length, 2);
// User scroll do not trigger the ScrollContentMetricsNotification.
expect(events[0] is UserScrollNotification, true);
expect(events[1] is ScrollUpdateNotification, true);

events.clear();
// Change the content dimensions again.
await tester.pumpWidget(buildFrame(500.0));
expect(events.length, 1);
event = events[0] as ScrollMetricsNotification;
expect(event.metrics.extentBefore, 10.0);
expect(event.metrics.extentInside, 590.0);
expect(event.metrics.extentAfter, 0.0);

events.clear();
// The content dimensions does not change.
await tester.pumpWidget(buildFrame(500.0));
expect(events.length, 0);
});

testWidgets('Scroll notification basics', (WidgetTester tester) async {
late ScrollNotification notification;

Expand Down