allow changing the paint offset of a GlowingOverscrollIndicator (#55829)
diff --git a/AUTHORS b/AUTHORS
index 4c673d6..a2cc16a 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -54,4 +54,5 @@
Michel Feinstein <michel@feinstein.com.br>
Michael Lee <ckmichael8@gmail.com>
Katarina Sheremet <katarina@sheremet.ch>
+Nicolas Schneider <nioncode+git@gmail.com>
Mikhail Zotyev <mbixjkee1392@gmail.com>
diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart
index aec670b..d6e8f16 100644
--- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart
+++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart
@@ -33,14 +33,53 @@
///
/// In a [MaterialApp], the edge glow color is the [ThemeData.accentColor].
///
+/// ## Customizing the Glow Position for Advanced Scroll Views
+///
/// When building a [CustomScrollView] with a [GlowingOverscrollIndicator], the
/// indicator will apply to the entire scrollable area, regardless of what
/// slivers the CustomScrollView contains.
///
/// For example, if your CustomScrollView contains a SliverAppBar in the first
/// position, the GlowingOverscrollIndicator will overlay the SliverAppBar. To
-/// manipulate the position of the GlowingOverscrollIndicator in this case, use
-/// a [NestedScrollView].
+/// manipulate the position of the GlowingOverscrollIndicator in this case,
+/// you can either make use of a [NotificationListener] and provide a
+/// [OverscrollIndicatorNotification.paintOffset] to the
+/// notification, or use a [NestedScrollView].
+///
+/// {@tool dartpad --template=stateless_widget_scaffold}
+///
+/// This example demonstrates how to use a [NotificationListener] to manipulate
+/// the placement of a [GlowingOverscrollIndicator] when building a
+/// [CustomScrollView]. Drag the scrollable to see the bounds of the overscroll
+/// indicator.
+///
+/// ```dart
+/// Widget build(BuildContext context) {
+/// double leadingPaintOffset = MediaQuery.of(context).padding.top + AppBar().preferredSize.height;
+/// return NotificationListener<OverscrollIndicatorNotification>(
+/// onNotification: (notification) {
+/// if (notification.leading) {
+/// notification.paintOffset = leadingPaintOffset;
+/// }
+/// return false;
+/// },
+/// child: CustomScrollView(
+/// slivers: [
+/// SliverAppBar(title: Text('Custom PaintOffset')),
+/// SliverToBoxAdapter(
+/// child: Container(
+/// color: Colors.amberAccent,
+/// height: 100,
+/// child: Center(child: Text('Glow all day!')),
+/// ),
+/// ),
+/// SliverFillRemaining(child: FlutterLogo()),
+/// ],
+/// ),
+/// );
+/// }
+/// ```
+/// {@end-tool}
///
/// {@tool dartpad --template=stateless_widget_scaffold}
///
@@ -73,6 +112,13 @@
/// }
/// ```
/// {@end-tool}
+///
+/// See also:
+///
+/// * [OverscrollIndicatorNotification], which can be used to manipulate the
+/// glow position or prevent the glow from being painted at all
+/// * [NotificationListener], to listen for the
+/// [OverscrollIndicatorNotification]
class GlowingOverscrollIndicator extends StatefulWidget {
/// Creates a visual indication that a scroll view has overscrolled.
///
@@ -199,6 +245,16 @@
bool _handleScrollNotification(ScrollNotification notification) {
if (!widget.notificationPredicate(notification))
return false;
+
+ // Update the paint offset with the current scroll position. This makes
+ // sure that the glow effect correctly scrolls in line with the current
+ // scroll, e.g. when scrolling in the opposite direction again to hide
+ // the glow. Otherwise, the glow would always stay in a fixed position,
+ // even if the top of the content already scrolled away.
+ _leadingController._paintOffsetScrollPixels = -notification.metrics.pixels;
+ _trailingController._paintOffsetScrollPixels =
+ -(notification.metrics.maxScrollExtent - notification.metrics.pixels);
+
if (notification is OverscrollNotification) {
_GlowController controller;
if (notification.overscroll < 0.0) {
@@ -213,6 +269,9 @@
final OverscrollIndicatorNotification confirmationNotification = OverscrollIndicatorNotification(leading: isLeading);
confirmationNotification.dispatch(context);
_accepted[isLeading] = confirmationNotification._accepted;
+ if (_accepted[isLeading]) {
+ controller._paintOffset = confirmationNotification.paintOffset;
+ }
}
assert(controller != null);
assert(notification.metrics.axis == widget.axis);
@@ -309,6 +368,8 @@
_GlowState _state = _GlowState.idle;
AnimationController _glowController;
Timer _pullRecedeTimer;
+ double _paintOffset = 0.0;
+ double _paintOffsetScrollPixels = 0.0;
// animation values
final Tween<double> _glowOpacityTween = Tween<double>(begin: 0.0, end: 0.0);
@@ -490,6 +551,7 @@
final Offset center = Offset((size.width / 2.0) * (0.5 + _displacement), height - radius);
final Paint paint = Paint()..color = color.withOpacity(_glowOpacity.value);
canvas.save();
+ canvas.translate(0.0, _paintOffset + _paintOffsetScrollPixels);
canvas.scale(1.0, scaleY);
canvas.clipRect(rect);
canvas.drawCircle(center, radius, paint);
@@ -588,6 +650,18 @@
/// view.
final bool leading;
+ /// Controls at which offset the glow should be drawn.
+ ///
+ /// A positive offset will move the glow away from its edge,
+ /// i.e. for a vertical, [leading] indicator, a [paintOffset] of 100.0 will
+ /// draw the indicator 100.0 pixels from the top of the edge.
+ /// For a vertical indicator with [leading] set to `false`, a [paintOffset]
+ /// of 100.0 will draw the indicator 100.0 pixels from the bottom instead.
+ ///
+ /// A negative [paintOffset] is generally not useful, since the glow will be
+ /// clipped.
+ double paintOffset = 0.0;
+
bool _accepted = true;
/// Call this method if the glow should be prevented.
diff --git a/packages/flutter/test/widgets/overscroll_indicator_test.dart b/packages/flutter/test/widgets/overscroll_indicator_test.dart
index c1a58f9..934f98c 100644
--- a/packages/flutter/test/widgets/overscroll_indicator_test.dart
+++ b/packages/flutter/test/widgets/overscroll_indicator_test.dart
@@ -320,6 +320,68 @@
expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A0000FF))..saveRestore());
expect(painter, isNot(paints..circle()..circle()));
});
+
+ group('Modify glow position', () {
+ testWidgets('Leading', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: NotificationListener<OverscrollIndicatorNotification>(
+ onNotification: (OverscrollIndicatorNotification notification) {
+ if (notification.leading) {
+ notification.paintOffset = 50.0;
+ }
+ return false;
+ },
+ child: const CustomScrollView(
+ slivers: <Widget>[
+ SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
+ ],
+ ),
+ ),
+ ),
+ );
+ final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
+ await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
+ expect(painter, paints..save()..translate(y: 50.0)..scale()..circle());
+ // Reverse scroll direction.
+ await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -30.0));
+ await tester.pump();
+ // The painter should follow the scroll direction.
+ expect(painter, paints..save()..translate(y: 50.0 - 30.0)..scale()..circle());
+ });
+
+ testWidgets('Trailing', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: NotificationListener<OverscrollIndicatorNotification>(
+ onNotification: (OverscrollIndicatorNotification notification) {
+ if (!notification.leading) {
+ notification.paintOffset = 50.0;
+ }
+ return false;
+ },
+ child: const CustomScrollView(
+ slivers: <Widget>[
+ SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
+ ],
+ ),
+ ),
+ ),
+ );
+ final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
+ await tester.dragFrom(const Offset(200.0, 200.0), const Offset(200.0, -10000.0));
+ await tester.pump();
+ await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -5.0));
+ expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0)..scale()..circle());
+ // Reverse scroll direction.
+ await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 30.0));
+ await tester.pump();
+ // The painter should follow the scroll direction.
+ expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 30.0)..scale()..circle());
+ });
+ });
}
class TestScrollBehavior1 extends ScrollBehavior {