Re-land "Added MaterialState.scrolledUnder and support in AppBar.backgroundColor" (#80395)
This commit is contained in:
parent
d1d80aa883
commit
2848126735
@ -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,6 +3238,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
|
||||
return _ScaffoldScope(
|
||||
hasDrawer: hasDrawer,
|
||||
geometryNotifier: _geometryNotifier,
|
||||
child: ScrollNotificationObserver(
|
||||
child: Material(
|
||||
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
|
||||
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
|
||||
@ -3260,6 +3261,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,174 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user