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 'icons.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'material_localizations.dart';
|
import 'material_localizations.dart';
|
||||||
|
import 'material_state.dart';
|
||||||
import 'scaffold.dart';
|
import 'scaffold.dart';
|
||||||
import 'tabs.dart';
|
import 'tabs.dart';
|
||||||
import 'text_theme.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
|
/// null, then [AppBar] uses the overall theme's [ColorScheme.primary] if the
|
||||||
/// overall theme's brightness is [Brightness.light], and [ColorScheme.surface]
|
/// overall theme's brightness is [Brightness.light], and [ColorScheme.surface]
|
||||||
/// if the overall theme's [brightness] is [Brightness.dark].
|
/// 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}
|
/// {@endtemplate}
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
@ -704,6 +709,28 @@ class _AppBarState extends State<AppBar> {
|
|||||||
static const double _defaultElevation = 4.0;
|
static const double _defaultElevation = 4.0;
|
||||||
static const Color _defaultShadowColor = Color(0xFF000000);
|
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() {
|
void _handleDrawerButton() {
|
||||||
Scaffold.of(context).openDrawer();
|
Scaffold.of(context).openDrawer();
|
||||||
}
|
}
|
||||||
@ -712,6 +739,24 @@ class _AppBarState extends State<AppBar> {
|
|||||||
Scaffold.of(context).openEndDrawer();
|
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) {
|
SystemUiOverlayStyle _systemOverlayStyleForBrightness(Brightness brightness) {
|
||||||
return brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark;
|
return brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark;
|
||||||
}
|
}
|
||||||
@ -726,6 +771,11 @@ class _AppBarState extends State<AppBar> {
|
|||||||
final ScaffoldState? scaffold = Scaffold.maybeOf(context);
|
final ScaffoldState? scaffold = Scaffold.maybeOf(context);
|
||||||
final ModalRoute<dynamic>? parentRoute = ModalRoute.of(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 hasDrawer = scaffold?.hasDrawer ?? false;
|
||||||
final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false;
|
final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false;
|
||||||
final bool canPop = parentRoute?.canPop ?? false;
|
final bool canPop = parentRoute?.canPop ?? false;
|
||||||
@ -738,9 +788,11 @@ class _AppBarState extends State<AppBar> {
|
|||||||
? widget.backgroundColor
|
? widget.backgroundColor
|
||||||
?? appBarTheme.backgroundColor
|
?? appBarTheme.backgroundColor
|
||||||
?? theme.primaryColor
|
?? theme.primaryColor
|
||||||
: widget.backgroundColor
|
: _resolveColor(
|
||||||
?? appBarTheme.backgroundColor
|
states,
|
||||||
?? (colorScheme.brightness == Brightness.dark ? colorScheme.surface : colorScheme.primary);
|
widget.backgroundColor,
|
||||||
|
appBarTheme.backgroundColor,
|
||||||
|
colorScheme.brightness == Brightness.dark ? colorScheme.surface : colorScheme.primary);
|
||||||
|
|
||||||
final Color foregroundColor = widget.foregroundColor
|
final Color foregroundColor = widget.foregroundColor
|
||||||
?? appBarTheme.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 extraToolbarHeight = math.max(minExtent - _bottomHeight - topPadding - (toolbarHeight ?? kToolbarHeight), 0.0);
|
||||||
final double visibleToolbarHeight = visibleMainHeight - _bottomHeight - extraToolbarHeight;
|
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 bool isPinnedWithOpacityFade = pinned && floating && bottom != null && extraToolbarHeight == 0.0;
|
||||||
final double toolbarOpacity = !pinned || isPinnedWithOpacityFade
|
final double toolbarOpacity = !pinned || isPinnedWithOpacityFade
|
||||||
? (visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight)).clamp(0.0, 1.0)
|
? (visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight)).clamp(0.0, 1.0)
|
||||||
@ -1155,6 +1208,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
maxExtent: maxExtent,
|
maxExtent: maxExtent,
|
||||||
currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
|
currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
|
||||||
toolbarOpacity: toolbarOpacity,
|
toolbarOpacity: toolbarOpacity,
|
||||||
|
isScrolledUnder: isScrolledUnder,
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
leading: leading,
|
leading: leading,
|
||||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||||
@ -1164,7 +1218,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
? Semantics(child: flexibleSpace, header: true)
|
? Semantics(child: flexibleSpace, header: true)
|
||||||
: flexibleSpace,
|
: flexibleSpace,
|
||||||
bottom: bottom,
|
bottom: bottom,
|
||||||
elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation : 0.0,
|
elevation: forceElevated || isScrolledUnder ? elevation : 0.0,
|
||||||
shadowColor: shadowColor,
|
shadowColor: shadowColor,
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
foregroundColor: foregroundColor,
|
foregroundColor: foregroundColor,
|
||||||
|
@ -210,8 +210,9 @@ class FlexibleSpaceBar extends StatefulWidget {
|
|||||||
/// height of the resulting [FlexibleSpaceBar] when fully expanded.
|
/// height of the resulting [FlexibleSpaceBar] when fully expanded.
|
||||||
/// `currentExtent` sets the scale of the [FlexibleSpaceBar.background] and
|
/// `currentExtent` sets the scale of the [FlexibleSpaceBar.background] and
|
||||||
/// [FlexibleSpaceBar.title] widgets of [FlexibleSpaceBar] upon
|
/// [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:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [FlexibleSpaceBarSettings] which creates a settings object that can be
|
/// * [FlexibleSpaceBarSettings] which creates a settings object that can be
|
||||||
@ -220,6 +221,7 @@ class FlexibleSpaceBar extends StatefulWidget {
|
|||||||
double? toolbarOpacity,
|
double? toolbarOpacity,
|
||||||
double? minExtent,
|
double? minExtent,
|
||||||
double? maxExtent,
|
double? maxExtent,
|
||||||
|
bool? isScrolledUnder,
|
||||||
required double currentExtent,
|
required double currentExtent,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) {
|
}) {
|
||||||
@ -228,6 +230,7 @@ class FlexibleSpaceBar extends StatefulWidget {
|
|||||||
toolbarOpacity: toolbarOpacity ?? 1.0,
|
toolbarOpacity: toolbarOpacity ?? 1.0,
|
||||||
minExtent: minExtent ?? currentExtent,
|
minExtent: minExtent ?? currentExtent,
|
||||||
maxExtent: maxExtent ?? currentExtent,
|
maxExtent: maxExtent ?? currentExtent,
|
||||||
|
isScrolledUnder: isScrolledUnder,
|
||||||
currentExtent: currentExtent,
|
currentExtent: currentExtent,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
@ -441,6 +444,7 @@ class FlexibleSpaceBarSettings extends InheritedWidget {
|
|||||||
required this.maxExtent,
|
required this.maxExtent,
|
||||||
required this.currentExtent,
|
required this.currentExtent,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
|
this.isScrolledUnder,
|
||||||
}) : assert(toolbarOpacity != null),
|
}) : assert(toolbarOpacity != null),
|
||||||
assert(minExtent != null && minExtent >= 0),
|
assert(minExtent != null && minExtent >= 0),
|
||||||
assert(maxExtent != null && maxExtent >= 0),
|
assert(maxExtent != null && maxExtent >= 0),
|
||||||
@ -465,11 +469,23 @@ class FlexibleSpaceBarSettings extends InheritedWidget {
|
|||||||
/// these elements upon initialization.
|
/// these elements upon initialization.
|
||||||
final double currentExtent;
|
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
|
@override
|
||||||
bool updateShouldNotify(FlexibleSpaceBarSettings oldWidget) {
|
bool updateShouldNotify(FlexibleSpaceBarSettings oldWidget) {
|
||||||
return toolbarOpacity != oldWidget.toolbarOpacity
|
return toolbarOpacity != oldWidget.toolbarOpacity
|
||||||
|| minExtent != oldWidget.minExtent
|
|| minExtent != oldWidget.minExtent
|
||||||
|| maxExtent != oldWidget.maxExtent
|
|| 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.
|
/// See: https://material.io/design/interaction/states.html#selected.
|
||||||
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
|
/// Disabled widgets should not respond to hover, focus, press, or drag
|
||||||
/// interactions.
|
/// interactions.
|
||||||
|
@ -3238,27 +3238,29 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
|
|||||||
return _ScaffoldScope(
|
return _ScaffoldScope(
|
||||||
hasDrawer: hasDrawer,
|
hasDrawer: hasDrawer,
|
||||||
geometryNotifier: _geometryNotifier,
|
geometryNotifier: _geometryNotifier,
|
||||||
child: Material(
|
child: ScrollNotificationObserver(
|
||||||
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
|
child: Material(
|
||||||
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
|
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
|
||||||
return CustomMultiChildLayout(
|
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
|
||||||
children: children,
|
return CustomMultiChildLayout(
|
||||||
delegate: _ScaffoldLayout(
|
children: children,
|
||||||
extendBody: _extendBody,
|
delegate: _ScaffoldLayout(
|
||||||
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
|
extendBody: _extendBody,
|
||||||
minInsets: minInsets,
|
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
|
||||||
minViewPadding: minViewPadding,
|
minInsets: minInsets,
|
||||||
currentFloatingActionButtonLocation: _floatingActionButtonLocation!,
|
minViewPadding: minViewPadding,
|
||||||
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
|
currentFloatingActionButtonLocation: _floatingActionButtonLocation!,
|
||||||
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
|
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
|
||||||
geometryNotifier: _geometryNotifier,
|
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
|
||||||
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!,
|
geometryNotifier: _geometryNotifier,
|
||||||
textDirection: textDirection,
|
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!,
|
||||||
isSnackBarFloating: isSnackBarFloating,
|
textDirection: textDirection,
|
||||||
snackBarWidth: snackBarWidth,
|
isSnackBarFloating: isSnackBarFloating,
|
||||||
),
|
snackBarWidth: snackBarWidth,
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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_controller.dart';
|
||||||
export 'src/widgets/scroll_metrics.dart';
|
export 'src/widgets/scroll_metrics.dart';
|
||||||
export 'src/widgets/scroll_notification.dart';
|
export 'src/widgets/scroll_notification.dart';
|
||||||
|
export 'src/widgets/scroll_notification_observer.dart';
|
||||||
export 'src/widgets/scroll_physics.dart';
|
export 'src/widgets/scroll_physics.dart';
|
||||||
export 'src/widgets/scroll_position.dart';
|
export 'src/widgets/scroll_position.dart';
|
||||||
export 'src/widgets/scroll_position_with_single_context.dart';
|
export 'src/widgets/scroll_position_with_single_context.dart';
|
||||||
|
@ -2560,4 +2560,266 @@ void main() {
|
|||||||
expect(actionIconTheme.color, foregroundColor);
|
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'
|
' PhysicalModel\n'
|
||||||
' AnimatedPhysicalModel\n'
|
' AnimatedPhysicalModel\n'
|
||||||
' Material\n'
|
' Material\n'
|
||||||
|
' _ScrollNotificationObserverScope\n'
|
||||||
|
' NotificationListener<ScrollNotification>\n'
|
||||||
|
' ScrollNotificationObserver\n'
|
||||||
' _ScaffoldScope\n'
|
' _ScaffoldScope\n'
|
||||||
' Scaffold\n'
|
' Scaffold\n'
|
||||||
' MediaQuery\n'
|
' MediaQuery\n'
|
||||||
|
@ -150,4 +150,69 @@ void main() {
|
|||||||
expect(notificationTypes, equals(types));
|
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