From c05a05e6fcc02977b90beb0a6704c2aa73df3575 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Thu, 25 May 2023 18:58:52 +0300 Subject: [PATCH] Add `ScrollNotificationObserver` sample (#127023) fixes https://github.com/flutter/flutter/issues/126702 ### Preview https://github.com/flutter/flutter/assets/48603081/4c529a0d-b8a5-4950-9095-429f1c5eccbb --- .../scroll_notification_observer.0.dart | 135 ++++++++++++++++++ .../scroll_notification_observer.0_test.dart | 31 ++++ .../widgets/scroll_notification_observer.dart | 9 ++ 3 files changed, 175 insertions(+) create mode 100644 examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart create mode 100644 examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart diff --git a/examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart b/examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart new file mode 100644 index 0000000000..78459bc59c --- /dev/null +++ b/examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart @@ -0,0 +1,135 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [ScrollNotificationObserver]. + +void main() => runApp(const ScrollNotificationObserverApp()); + +class ScrollNotificationObserverApp extends StatelessWidget { + const ScrollNotificationObserverApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + // The Scaffold widget contains a [ScrollNotificationObserver]. + // This is used by [AppBar] for its scrolled under behavior. + // + // We can use [ScrollNotificationObserver.maybeOf] to get the + // state of this [ScrollNotificationObserver] from descendants + // of the Scaffold widget. + // + // If you're not using a [Scaffold] widget, you can create a [ScrollNotificationObserver] + // to notify its descendants of scroll notifications by adding it to the subtree. + home: Scaffold( + appBar: AppBar( + title: const Text('ScrollNotificationObserver Sample'), + ), + body: const ScrollNotificationObserverExample(), + ), + ); + } +} + +class ScrollNotificationObserverExample extends StatefulWidget { + const ScrollNotificationObserverExample({super.key}); + + @override + State createState() => _ScrollNotificationObserverExampleState(); +} + +class _ScrollNotificationObserverExampleState extends State { + ScrollNotificationObserverState? _scrollNotificationObserver; + ScrollController controller = ScrollController(); + bool _scrolledDown = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Remove any previous listener. + _scrollNotificationObserver?.removeListener(_handleScrollNotification); + // Get the ScrollNotificationObserverState from the Scaffold widget. + _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); + // Add a new listener. + _scrollNotificationObserver?.addListener(_handleScrollNotification); + } + + @override + void dispose() { + if (_scrollNotificationObserver != null) { + _scrollNotificationObserver!.removeListener(_handleScrollNotification); + _scrollNotificationObserver = null; + } + controller.dispose(); + super.dispose(); + } + + void _handleScrollNotification(ScrollNotification notification) { + // Check if the notification is a scroll update notification and if the + // `notification.depth` is 0. This way we only listen to the scroll + // notifications from the closest scrollable, instead of those that may be nested. + if (notification is ScrollUpdateNotification && defaultScrollNotificationPredicate(notification)) { + final ScrollMetrics metrics = notification.metrics; + // Check if the user scrolled down. + if (_scrolledDown != metrics.extentBefore > 0) { + setState(() { + _scrolledDown = metrics.extentBefore > 0; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + SampleList(controller: controller), + // Show the button only if the user scrolled down. + if (_scrolledDown) + Positioned( + right: 25, + bottom: 20, + child: Center( + child: GestureDetector( + onTap: () { + // Scroll to the top when the user taps the button. + controller.animateTo(0, duration: const Duration(milliseconds: 200), curve:Curves.fastOutSlowIn); + }, + child: const Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + Icon(Icons.arrow_upward_rounded), + Text('Scroll to top') + ], + ), + ), + ), + ), + ), + ), + ], + ); + } +} + +class SampleList extends StatelessWidget { + const SampleList({super.key, required this.controller}); + + final ScrollController controller; + + @override + Widget build(BuildContext context) { + return ListView.builder( + controller: controller, + itemCount: 30, + itemBuilder: (BuildContext context, int index) { + return ListTile(title: Text('Item $index')); + }, + ); + } +} diff --git a/examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart b/examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart new file mode 100644 index 0000000000..0828976417 --- /dev/null +++ b/examples/api/test/widgets/scroll_notification_observer/scroll_notification_observer.0_test.dart @@ -0,0 +1,31 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/widgets/scroll_notification_observer/scroll_notification_observer.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Scroll to top buttons appears when scrolling down', (WidgetTester tester) async { + const String buttonText = 'Scroll to top'; + + await tester.pumpWidget( + const example.ScrollNotificationObserverApp(), + ); + + expect(find.byType(ScrollNotificationObserver), findsOneWidget); + expect(find.text(buttonText), findsNothing); + + // Scroll down. + await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); + await tester.pumpAndSettle(); + + expect(find.text(buttonText), findsOneWidget); + + await tester.tap(find.text(buttonText)); + await tester.pumpAndSettle(); + + expect(find.text(buttonText), findsNothing); + }); +} diff --git a/packages/flutter/lib/src/widgets/scroll_notification_observer.dart b/packages/flutter/lib/src/widgets/scroll_notification_observer.dart index 51fcf07ac1..4419269da8 100644 --- a/packages/flutter/lib/src/widgets/scroll_notification_observer.dart +++ b/packages/flutter/lib/src/widgets/scroll_notification_observer.dart @@ -71,6 +71,15 @@ final class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { /// This widget is similar to [NotificationListener]. It supports a listener /// list instead of just a single listener and its listeners run /// unconditionally, they do not require a gating boolean return value. +/// +/// {@tool dartpad} +/// This sample shows a "Scroll to top" button that uses [ScrollNotificationObserver] +/// to listen for scroll notifications from [ListView]. The button is only visible +/// when the user has scrolled down. When pressed, the button animates the scroll +/// position of the [ListView] back to the top. +/// +/// ** See code in examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart ** +/// {@end-tool} class ScrollNotificationObserver extends StatefulWidget { /// Create a [ScrollNotificationObserver]. ///