mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[new feature]introduce ScrollMetricsNotification (#85221)
This commit is contained in:
parent
4914a4090c
commit
17db4e207c
@ -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';
|
||||
@ -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';
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
///
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user