Unverified Commit c7d29350 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Fix reverse cases for App Bar scrolled under behavior (#101460)

parent f5f9ad91
......@@ -737,24 +737,24 @@ class _AppBarState extends State<AppBar> {
static const double _defaultElevation = 4.0;
static const Color _defaultShadowColor = Color(0xFF000000);
ScrollNotificationObserverState? _scrollNotificationObserver;
ScrollMetricsNotificationObserverState? _scrollMetricsNotificationObserver;
bool _scrolledUnder = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_scrollNotificationObserver != null)
_scrollNotificationObserver!.removeListener(_handleScrollNotification);
_scrollNotificationObserver = ScrollNotificationObserver.of(context);
if (_scrollNotificationObserver != null)
_scrollNotificationObserver!.addListener(_handleScrollNotification);
if (_scrollMetricsNotificationObserver != null)
_scrollMetricsNotificationObserver!.removeListener(_handleScrollMetricsNotification);
_scrollMetricsNotificationObserver = ScrollMetricsNotificationObserver.of(context);
if (_scrollMetricsNotificationObserver != null)
_scrollMetricsNotificationObserver!.addListener(_handleScrollMetricsNotification);
}
@override
void dispose() {
if (_scrollNotificationObserver != null) {
_scrollNotificationObserver!.removeListener(_handleScrollNotification);
_scrollNotificationObserver = null;
if (_scrollMetricsNotificationObserver != null) {
_scrollMetricsNotificationObserver!.removeListener(_handleScrollMetricsNotification);
_scrollMetricsNotificationObserver = null;
}
super.dispose();
}
......@@ -767,18 +767,34 @@ 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
&& notification.metrics.axis == Axis.vertical;
if (_scrolledUnder != oldScrolledUnder) {
setState(() {
// React to a change in MaterialState.scrolledUnder
});
void _handleScrollMetricsNotification(ScrollMetricsNotification notification) {
final bool oldScrolledUnder = _scrolledUnder;
final ScrollMetrics metrics = notification.metrics;
if (notification.depth != 0) {
_scrolledUnder = false;
} else {
switch (metrics.axisDirection) {
case AxisDirection.up:
// Scroll view is reversed
_scrolledUnder = metrics.extentAfter > 0;
break;
case AxisDirection.down:
_scrolledUnder = metrics.extentBefore > 0;
break;
case AxisDirection.right:
case AxisDirection.left:
// Scrolled under is only supported in the vertical axis.
_scrolledUnder = false;
break;
}
}
if (_scrolledUnder != oldScrolledUnder) {
setState(() {
// React to a change in MaterialState.scrolledUnder
});
}
}
Color _resolveColor(Set<MaterialState> states, Color? widgetColor, Color? themeColor, Color defaultColor) {
......
......@@ -3096,7 +3096,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
return _ScaffoldScope(
hasDrawer: hasDrawer,
geometryNotifier: _geometryNotifier,
child: ScrollNotificationObserver(
child: ScrollMetricsNotificationObserver(
child: Material(
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
......
......@@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'scroll_notification.dart';
import 'scroll_position.dart';
/// A [ScrollNotification] listener for [ScrollNotificationObserver].
///
......@@ -172,3 +173,170 @@ class ScrollNotificationObserverState extends State<ScrollNotificationObserver>
super.dispose();
}
}
/// A [ScrollMetricsNotification] listener for [ScrollMetricsNotificationObserver].
///
/// [ScrollMetricsNotificationObserver] 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 ScrollMetricsNotificationCallback = void Function(ScrollMetricsNotification notification);
class _ScrollMetricsNotificationObserverScope extends InheritedWidget {
const _ScrollMetricsNotificationObserverScope({
Key? key,
required Widget child,
required ScrollMetricsNotificationObserverState scrollMetricsNotificationObserverState,
}) : _scrollMetricsNotificationObserverState = scrollMetricsNotificationObserverState,
super(key: key, child: child);
final ScrollMetricsNotificationObserverState _scrollMetricsNotificationObserverState;
@override
bool updateShouldNotify(_ScrollMetricsNotificationObserverScope old) {
return _scrollMetricsNotificationObserverState != old._scrollMetricsNotificationObserverState;
}
}
class _MetricsListenerEntry extends LinkedListEntry<_MetricsListenerEntry> {
_MetricsListenerEntry(this.listener);
final ScrollMetricsNotificationCallback listener;
}
/// Notifies its listeners when a descendant ScrollMetrics are
/// initialized or updated.
///
/// To add a listener to a [ScrollMetricsNotificationObserver] ancestor:
/// ```dart
/// void listener(ScrollMetricsNotification notification) {
/// // Do something, maybe setState()
/// }
/// ScrollMetricsNotificationObserver.of(context).addListener(listener)
/// ```
///
/// To remove the listener from a [ScrollMetricsNotificationObserver] ancestor:
/// ```dart
/// ScrollMetricsNotificationObserver.of(context).removeListener(listener);
/// ```
///
/// Stateful widgets that share an ancestor [ScrollMetricsNotificationObserver]
/// 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 ScrollMetricsNotificationObserver extends StatefulWidget {
/// Create a [ScrollMetricsNotificationObserver].
///
/// The [child] parameter must not be null.
const ScrollMetricsNotificationObserver({
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 [ScrollMetricsNotificationObserver] widget, then
/// null is returned.
static ScrollMetricsNotificationObserverState? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_ScrollMetricsNotificationObserverScope>()?._scrollMetricsNotificationObserverState;
}
@override
ScrollMetricsNotificationObserverState createState() => ScrollMetricsNotificationObserverState();
}
/// The listener list state for a [ScrollMetricsNotificationObserver] returned
/// by [ScrollMetricsNotificationObserver.of].
///
/// [ScrollMetricsNotificationObserver] 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 ScrollMetricsNotificationObserverState extends State<ScrollMetricsNotificationObserver> {
LinkedList<_MetricsListenerEntry>? _listeners = LinkedList<_MetricsListenerEntry>();
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 [ScrollMetricsNotificationCallback] that will be called each time
/// a descendant scrolls.
void addListener(ScrollMetricsNotificationCallback listener) {
assert(_debugAssertNotDisposed());
_listeners!.add(_MetricsListenerEntry(listener));
}
/// Remove the specified [ScrollMetricsNotificationCallback].
void removeListener(ScrollMetricsNotificationCallback listener) {
assert(_debugAssertNotDisposed());
for (final _MetricsListenerEntry entry in _listeners!) {
if (entry.listener == listener) {
entry.unlink();
return;
}
}
}
void _notifyListeners(ScrollMetricsNotification notification) {
assert(_debugAssertNotDisposed());
if (_listeners!.isEmpty)
return;
final List<_MetricsListenerEntry> localListeners = List<_MetricsListenerEntry>.of(_listeners!);
for (final _MetricsListenerEntry 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: () => <DiagnosticsNode>[
DiagnosticsProperty<ScrollMetricsNotificationObserverState>(
'The $runtimeType sending notification was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
],
));
}
}
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollMetricsNotification>(
onNotification: (ScrollMetricsNotification notification) {
_notifyListeners(notification);
return false;
},
child: _ScrollMetricsNotificationObserverScope(
scrollMetricsNotificationObserverState: this,
child: widget.child,
),
);
}
@override
void dispose() {
assert(_debugAssertNotDisposed());
_listeners = null;
super.dispose();
}
}
......@@ -2302,9 +2302,9 @@ void main() {
' PhysicalModel\n'
' AnimatedPhysicalModel\n'
' Material\n'
' _ScrollNotificationObserverScope\n'
' NotificationListener<ScrollNotification>\n'
' ScrollNotificationObserver\n'
' _ScrollMetricsNotificationObserverScope\n'
' NotificationListener<ScrollMetricsNotification>\n'
' ScrollMetricsNotificationObserver\n'
' _ScaffoldScope\n'
' Scaffold\n'
' MediaQuery\n'
......
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