Unverified Commit 28481267 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Re-land "Added MaterialState.scrolledUnder and support in AppBar.backgroundColor" (#80395)

parent d1d80aa8
......@@ -19,6 +19,7 @@ import 'icon_button.dart';
import 'icons.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'material_state.dart';
import 'scaffold.dart';
import 'tabs.dart';
import 'text_theme.dart';
......@@ -414,6 +415,10 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// null, then [AppBar] uses the overall theme's [ColorScheme.primary] if the
/// overall theme's brightness is [Brightness.light], and [ColorScheme.surface]
/// if the overall theme's [brightness] is [Brightness.dark].
///
/// If this color is a [MaterialStateColor] it will be resolved against
/// [MaterialState.scrolledUnder] when the content of the app's
/// primary scrollable overlaps the app bar.
/// {@endtemplate}
///
/// See also:
......@@ -704,6 +709,28 @@ class _AppBarState extends State<AppBar> {
static const double _defaultElevation = 4.0;
static const Color _defaultShadowColor = Color(0xFF000000);
ScrollNotificationObserverState? _scrollNotificationObserver;
bool _scrolledUnder = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_scrollNotificationObserver != null)
_scrollNotificationObserver!.removeListener(_handleScrollNotification);
_scrollNotificationObserver = ScrollNotificationObserver.of(context);
if (_scrollNotificationObserver != null)
_scrollNotificationObserver!.addListener(_handleScrollNotification);
}
@override
void dispose() {
if (_scrollNotificationObserver != null) {
_scrollNotificationObserver!.removeListener(_handleScrollNotification);
_scrollNotificationObserver = null;
}
super.dispose();
}
void _handleDrawerButton() {
Scaffold.of(context).openDrawer();
}
......@@ -712,6 +739,24 @@ class _AppBarState extends State<AppBar> {
Scaffold.of(context).openEndDrawer();
}
void _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
final bool oldScrolledUnder = _scrolledUnder;
_scrolledUnder = notification.depth == 0 && notification.metrics.extentBefore > 0;
if (_scrolledUnder != oldScrolledUnder) {
setState(() {
// React to a change in MaterialState.scrolledUnder
});
}
}
}
Color _resolveColor(Set<MaterialState> states, Color? widgetColor, Color? themeColor, Color defaultColor) {
return MaterialStateProperty.resolveAs<Color?>(widgetColor, states)
?? MaterialStateProperty.resolveAs<Color?>(themeColor, states)
?? MaterialStateProperty.resolveAs<Color>(defaultColor, states);
}
SystemUiOverlayStyle _systemOverlayStyleForBrightness(Brightness brightness) {
return brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark;
}
......@@ -726,6 +771,11 @@ class _AppBarState extends State<AppBar> {
final ScaffoldState? scaffold = Scaffold.maybeOf(context);
final ModalRoute<dynamic>? parentRoute = ModalRoute.of(context);
final FlexibleSpaceBarSettings? settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
final Set<MaterialState> states = <MaterialState>{
if (settings?.isScrolledUnder ?? _scrolledUnder) MaterialState.scrolledUnder,
};
final bool hasDrawer = scaffold?.hasDrawer ?? false;
final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false;
final bool canPop = parentRoute?.canPop ?? false;
......@@ -738,9 +788,11 @@ class _AppBarState extends State<AppBar> {
? widget.backgroundColor
?? appBarTheme.backgroundColor
?? theme.primaryColor
: widget.backgroundColor
?? appBarTheme.backgroundColor
?? (colorScheme.brightness == Brightness.dark ? colorScheme.surface : colorScheme.primary);
: _resolveColor(
states,
widget.backgroundColor,
appBarTheme.backgroundColor,
colorScheme.brightness == Brightness.dark ? colorScheme.surface : colorScheme.primary);
final Color foregroundColor = widget.foregroundColor
?? appBarTheme.foregroundColor
......@@ -1145,6 +1197,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double extraToolbarHeight = math.max(minExtent - _bottomHeight - topPadding - (toolbarHeight ?? kToolbarHeight), 0.0);
final double visibleToolbarHeight = visibleMainHeight - _bottomHeight - extraToolbarHeight;
final bool isScrolledUnder = overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent);
final bool isPinnedWithOpacityFade = pinned && floating && bottom != null && extraToolbarHeight == 0.0;
final double toolbarOpacity = !pinned || isPinnedWithOpacityFade
? (visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight)).clamp(0.0, 1.0)
......@@ -1155,6 +1208,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
maxExtent: maxExtent,
currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: toolbarOpacity,
isScrolledUnder: isScrolledUnder,
child: AppBar(
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
......@@ -1164,7 +1218,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
? Semantics(child: flexibleSpace, header: true)
: flexibleSpace,
bottom: bottom,
elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation : 0.0,
elevation: forceElevated || isScrolledUnder ? elevation : 0.0,
shadowColor: shadowColor,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
......
......@@ -210,8 +210,9 @@ class FlexibleSpaceBar extends StatefulWidget {
/// height of the resulting [FlexibleSpaceBar] when fully expanded.
/// `currentExtent` sets the scale of the [FlexibleSpaceBar.background] and
/// [FlexibleSpaceBar.title] widgets of [FlexibleSpaceBar] upon
/// initialization.
///
/// initialization. `scrolledUnder` is true if the the [FlexibleSpaceBar]
/// overlaps the app's primary scrollable, false if it does not, and null
/// if the caller has not determined as much.
/// See also:
///
/// * [FlexibleSpaceBarSettings] which creates a settings object that can be
......@@ -220,6 +221,7 @@ class FlexibleSpaceBar extends StatefulWidget {
double? toolbarOpacity,
double? minExtent,
double? maxExtent,
bool? isScrolledUnder,
required double currentExtent,
required Widget child,
}) {
......@@ -228,6 +230,7 @@ class FlexibleSpaceBar extends StatefulWidget {
toolbarOpacity: toolbarOpacity ?? 1.0,
minExtent: minExtent ?? currentExtent,
maxExtent: maxExtent ?? currentExtent,
isScrolledUnder: isScrolledUnder,
currentExtent: currentExtent,
child: child,
);
......@@ -441,6 +444,7 @@ class FlexibleSpaceBarSettings extends InheritedWidget {
required this.maxExtent,
required this.currentExtent,
required Widget child,
this.isScrolledUnder,
}) : assert(toolbarOpacity != null),
assert(minExtent != null && minExtent >= 0),
assert(maxExtent != null && maxExtent >= 0),
......@@ -465,11 +469,23 @@ class FlexibleSpaceBarSettings extends InheritedWidget {
/// these elements upon initialization.
final double currentExtent;
/// True if the FlexibleSpaceBar overlaps the primary scrollable's contents.
///
/// This value is used by the [AppBar] to resolve
/// [AppBar.backgroundColor] against [MaterialState.scrolledUnder],
/// i.e. to enable apps to specify different colors when content
/// has been scrolled up and behind the app bar.
///
/// Null if the caller hasn't determined if the FlexibleSpaceBar
/// overlaps the primary scrollable's contents.
final bool? isScrolledUnder;
@override
bool updateShouldNotify(FlexibleSpaceBarSettings oldWidget) {
return toolbarOpacity != oldWidget.toolbarOpacity
|| minExtent != oldWidget.minExtent
|| maxExtent != oldWidget.maxExtent
|| currentExtent != oldWidget.currentExtent;
|| currentExtent != oldWidget.currentExtent
|| isScrolledUnder != oldWidget.isScrolledUnder;
}
}
......@@ -63,7 +63,13 @@ enum MaterialState {
/// See: https://material.io/design/interaction/states.html#selected.
selected,
/// The state when this widget disabled and can not be interacted with.
/// The state when this widget overlaps the content of a scrollable below.
///
/// Used by [AppBar] to indicate that the primary scrollable's
/// content has scrolled up and behind the app bar.
scrolledUnder,
/// The state when this widget is disabled and cannot be interacted with.
///
/// Disabled widgets should not respond to hover, focus, press, or drag
/// interactions.
......
......@@ -3238,27 +3238,29 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
return _ScaffoldScope(
hasDrawer: hasDrawer,
geometryNotifier: _geometryNotifier,
child: Material(
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
return CustomMultiChildLayout(
children: children,
delegate: _ScaffoldLayout(
extendBody: _extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
minInsets: minInsets,
minViewPadding: minViewPadding,
currentFloatingActionButtonLocation: _floatingActionButtonLocation!,
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
geometryNotifier: _geometryNotifier,
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!,
textDirection: textDirection,
isSnackBarFloating: isSnackBarFloating,
snackBarWidth: snackBarWidth,
),
);
}),
child: ScrollNotificationObserver(
child: Material(
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
return CustomMultiChildLayout(
children: children,
delegate: _ScaffoldLayout(
extendBody: _extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
minInsets: minInsets,
minViewPadding: minViewPadding,
currentFloatingActionButtonLocation: _floatingActionButtonLocation!,
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
geometryNotifier: _geometryNotifier,
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!,
textDirection: textDirection,
isSnackBarFloating: isSnackBarFloating,
snackBarWidth: snackBarWidth,
),
);
}),
),
),
);
}
......
// 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 'dart:collection';
import 'package:flutter/foundation.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'scroll_notification.dart';
/// A [ScrollNotification] listener for [ScrollNotificationObserver].
///
/// [ScrollNotificationObserver] 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.
typedef ScrollNotificationCallback = void Function(ScrollNotification notification);
class _ScrollNotificationObserverScope extends InheritedWidget {
const _ScrollNotificationObserverScope({
Key? key,
required Widget child,
required ScrollNotificationObserverState scrollNotificationObserverState,
}) : _scrollNotificationObserverState = scrollNotificationObserverState,
super(key: key, child: child);
final ScrollNotificationObserverState _scrollNotificationObserverState;
@override
bool updateShouldNotify(_ScrollNotificationObserverScope old) => _scrollNotificationObserverState != old._scrollNotificationObserverState;
}
class _ListenerEntry extends LinkedListEntry<_ListenerEntry> {
_ListenerEntry(this.listener);
final ScrollNotificationCallback listener;
}
/// Notifies its listeners when a descendant scrolls.
///
/// To add a listener to a [ScrollNotificationObserver] ancestor:
/// ```dart
/// void listener(ScrollNotification notification) {
/// // Do something, maybe setState()
/// }
/// ScrollNotificationObserver.of(context).addListener(listener)
/// ```
///
/// To remove the listener from a [ScrollNotificationObserver] ancestor:
/// ```dart
/// ScrollNotificationObserver.of(context).removeListener(listener);
///```
///
/// Stateful widgets that share an ancestor [ScrollNotificationObserver] typically
/// add a listener in [State.didChangeDependencies] (removing the old one
/// if necessary) and remove the listener in their [State.dispose] method.
///
/// 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.
class ScrollNotificationObserver extends StatefulWidget {
/// Create a [ScrollNotificationObserver].
///
/// The [child] parameter must not be null.
const ScrollNotificationObserver({
Key? key,
required this.child,
}) : assert(child != null), super(key: key);
/// The subtree below this widget.
final Widget child;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [ScrollNotificationObserver] widget, then null is returned.
static ScrollNotificationObserverState? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_ScrollNotificationObserverScope>()?._scrollNotificationObserverState;
}
@override
ScrollNotificationObserverState createState() => ScrollNotificationObserverState();
}
/// The listener list state for a [ScrollNotificationObserver] returned by
/// [ScrollNotificationObserver.of].
///
/// [ScrollNotificationObserver] 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.
class ScrollNotificationObserverState extends State<ScrollNotificationObserver> {
LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>();
bool _debugAssertNotDisposed() {
assert(() {
if (_listeners == null) {
throw FlutterError(
'A $runtimeType was used after being disposed.\n'
'Once you have called dispose() on a $runtimeType, it can no longer be used.',
);
}
return true;
}());
return true;
}
/// Add a [ScrollNotificationCallback] that will be called each time
/// a descendant scrolls.
void addListener(ScrollNotificationCallback listener) {
assert(_debugAssertNotDisposed());
_listeners!.add(_ListenerEntry(listener));
}
/// Remove the specified [ScrollNotificationCallback].
void removeListener(ScrollNotificationCallback listener) {
assert(_debugAssertNotDisposed());
for (final _ListenerEntry entry in _listeners!) {
if (entry.listener == listener) {
entry.unlink();
return;
}
}
}
void _notifyListeners(ScrollNotification notification) {
assert(_debugAssertNotDisposed());
if (_listeners!.isEmpty)
return;
final List<_ListenerEntry> localListeners = List<_ListenerEntry>.from(_listeners!);
for (final _ListenerEntry entry in localListeners) {
try {
if (entry.list != null)
entry.listener(notification);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widget library',
context: ErrorDescription('while dispatching notifications for $runtimeType'),
informationCollector: () sync* {
yield DiagnosticsProperty<ScrollNotificationObserverState>(
'The $runtimeType sending notification was',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
));
}
}
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
_notifyListeners(notification);
return false;
},
child: _ScrollNotificationObserverScope(
scrollNotificationObserverState: this,
child: widget.child,
),
);
}
@override
void dispose() {
assert(_debugAssertNotDisposed());
_listeners = null;
super.dispose();
}
}
......@@ -98,6 +98,7 @@ export 'src/widgets/scroll_context.dart';
export 'src/widgets/scroll_controller.dart';
export 'src/widgets/scroll_metrics.dart';
export 'src/widgets/scroll_notification.dart';
export 'src/widgets/scroll_notification_observer.dart';
export 'src/widgets/scroll_physics.dart';
export 'src/widgets/scroll_position.dart';
export 'src/widgets/scroll_position_with_single_context.dart';
......
......@@ -2560,4 +2560,266 @@ void main() {
expect(actionIconTheme.color, foregroundColor);
});
testWidgets('SliverAppBar.backgroundColor MaterialStateColor scrolledUnder', (WidgetTester tester) async {
const double collapsedHeight = kToolbarHeight;
const double expandedHeight = 200.0;
const Color scrolledColor = Color(0xff00ff00);
const Color defaultColor = Color(0xff0000ff);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
backwardsCompatibility: false,
elevation: 0,
backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
return states.contains(MaterialState.scrolledUnder) ? scrolledColor : defaultColor;
}),
expandedHeight: expandedHeight,
pinned: true,
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
Container(height: 1200.0, color: Colors.teal),
],
),
),
],
),
),
),
);
Finder findAppBarMaterial() {
return find.descendant(of: find.byType(AppBar), matching: find.byType(Material));
}
Color? getAppBarBackgroundColor() {
return tester.widget<Material>(findAppBarMaterial()).color;
}
expect(getAppBarBackgroundColor(), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
});
testWidgets('SliverAppBar.backgroundColor with FlexibleSpace MaterialStateColor scrolledUnder', (WidgetTester tester) async {
const double collapsedHeight = kToolbarHeight;
const double expandedHeight = 200.0;
const Color scrolledColor = Color(0xff00ff00);
const Color defaultColor = Color(0xff0000ff);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
backwardsCompatibility: false,
elevation: 0,
backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
return states.contains(MaterialState.scrolledUnder) ? scrolledColor : defaultColor;
}),
expandedHeight: expandedHeight,
pinned: true,
flexibleSpace: const FlexibleSpaceBar(
title: Text('SliverAppBar'),
),
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
Container(height: 1200.0, color: Colors.teal),
],
),
),
],
),
),
),
);
Finder findAppBarMaterial() {
// There are 2 Material widgets below AppBar. The second is only added if
// flexibleSpace is non-null.
return find.descendant(of: find.byType(AppBar), matching: find.byType(Material)).first;
}
Color? getAppBarBackgroundColor() {
return tester.widget<Material>(findAppBarMaterial()).color;
}
expect(getAppBarBackgroundColor(), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
});
testWidgets('AppBar.backgroundColor MaterialStateColor scrolledUnder', (WidgetTester tester) async {
const Color scrolledColor = Color(0xff00ff00);
const Color defaultColor = Color(0xff0000ff);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
backwardsCompatibility: false,
elevation: 0,
backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
return states.contains(MaterialState.scrolledUnder) ? scrolledColor : defaultColor;
}),
title: const Text('AppBar'),
),
body: ListView(
children: <Widget>[
Container(height: 1200.0, color: Colors.teal),
],
),
),
),
);
Finder findAppBarMaterial() {
return find.descendant(of: find.byType(AppBar), matching: find.byType(Material));
}
Color? getAppBarBackgroundColor() {
return tester.widget<Material>(findAppBarMaterial()).color;
}
expect(getAppBarBackgroundColor(), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('AppBar.backgroundColor with FlexibleSpace MaterialStateColor scrolledUnder', (WidgetTester tester) async {
const Color scrolledColor = Color(0xff00ff00);
const Color defaultColor = Color(0xff0000ff);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
backwardsCompatibility: false,
elevation: 0,
backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
return states.contains(MaterialState.scrolledUnder) ? scrolledColor : defaultColor;
}),
title: const Text('AppBar'),
flexibleSpace: const FlexibleSpaceBar(
title: Text('FlexibleSpace'),
),
),
body: ListView(
children: <Widget>[
Container(height: 1200.0, color: Colors.teal),
],
),
),
),
);
Finder findAppBarMaterial() {
// There are 2 Material widgets below AppBar. The second is only added if
// flexibleSpace is non-null.
return find.descendant(of: find.byType(AppBar), matching: find.byType(Material)).first;
}
Color? getAppBarBackgroundColor() {
return tester.widget<Material>(findAppBarMaterial()).color;
}
expect(getAppBarBackgroundColor(), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, kToolbarHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight);
});
testWidgets('AppBar._handleScrollNotification safely calls setState()', (WidgetTester tester) async {
// Regression test for failures found in Google internal issue b/185192049.
final ScrollController controller = ScrollController(initialScrollOffset: 400);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
backwardsCompatibility: false,
title: const Text('AppBar'),
),
body: Scrollbar(
isAlwaysShown: true,
controller: controller,
child: ListView(
controller: controller,
children: <Widget>[
Container(height: 1200.0, color: Colors.teal),
],
),
),
),
),
);
expect(tester.takeException(), isNull);
});
}
......@@ -2190,6 +2190,9 @@ void main() {
' PhysicalModel\n'
' AnimatedPhysicalModel\n'
' Material\n'
' _ScrollNotificationObserverScope\n'
' NotificationListener<ScrollNotification>\n'
' ScrollNotificationObserver\n'
' _ScaffoldScope\n'
' Scaffold\n'
' MediaQuery\n'
......
......@@ -150,4 +150,69 @@ void main() {
expect(notificationTypes, equals(types));
});
testWidgets('ScrollNotificationObserver', (WidgetTester tester) async {
late ScrollNotificationObserverState observer;
ScrollNotification? notification;
void handleNotification(ScrollNotification value) {
if (value is ScrollStartNotification || value is ScrollUpdateNotification || value is ScrollEndNotification)
notification = value;
}
await tester.pumpWidget(
ScrollNotificationObserver(
child: Builder(
builder: (BuildContext context) {
observer = ScrollNotificationObserver.of(context)!;
return const SingleChildScrollView(
child: SizedBox(height: 1200.0),
);
},
),
),
);
observer.addListener(handleNotification);
TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
await tester.pumpAndSettle();
expect(notification, isA<ScrollStartNotification>());
expect(notification!.depth, equals(0));
final ScrollStartNotification start = notification! as ScrollStartNotification;
expect(start.dragDetails, isNotNull);
expect(start.dragDetails!.globalPosition, equals(const Offset(100.0, 100.0)));
await gesture.moveBy(const Offset(-10.0, -10.0));
await tester.pumpAndSettle();
expect(notification, isA<ScrollUpdateNotification>());
expect(notification!.depth, equals(0));
final ScrollUpdateNotification update = notification! as ScrollUpdateNotification;
expect(update.dragDetails, isNotNull);
expect(update.dragDetails!.globalPosition, equals(const Offset(90.0, 90.0)));
expect(update.dragDetails!.delta, equals(const Offset(0.0, -10.0)));
await gesture.up();
await tester.pumpAndSettle();
expect(notification, isA<ScrollEndNotification>());
expect(notification!.depth, equals(0));
final ScrollEndNotification end = notification! as ScrollEndNotification;
expect(end.dragDetails, isNotNull);
expect(end.dragDetails!.velocity, equals(Velocity.zero));
observer.removeListener(handleNotification);
notification = null;
gesture = await tester.startGesture(const Offset(100.0, 100.0));
await tester.pumpAndSettle();
expect(notification, isNull);
await gesture.moveBy(const Offset(-10.0, -10.0));
await tester.pumpAndSettle();
expect(notification, isNull);
await gesture.up();
await tester.pumpAndSettle();
expect(notification, isNull);
});
}
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