Unverified Commit 362a5573 authored by Nicolas Schneider's avatar Nicolas Schneider Committed by GitHub

allow changing the paint offset of a GlowingOverscrollIndicator (#55829)

parent 93d7af73
...@@ -54,4 +54,5 @@ Cédric Wyss <cedi.wyss@gmail.com> ...@@ -54,4 +54,5 @@ Cédric Wyss <cedi.wyss@gmail.com>
Michel Feinstein <michel@feinstein.com.br> Michel Feinstein <michel@feinstein.com.br>
Michael Lee <ckmichael8@gmail.com> Michael Lee <ckmichael8@gmail.com>
Katarina Sheremet <katarina@sheremet.ch> Katarina Sheremet <katarina@sheremet.ch>
Nicolas Schneider <nioncode+git@gmail.com>
Mikhail Zotyev <mbixjkee1392@gmail.com> Mikhail Zotyev <mbixjkee1392@gmail.com>
...@@ -33,14 +33,53 @@ import 'ticker_provider.dart'; ...@@ -33,14 +33,53 @@ import 'ticker_provider.dart';
/// ///
/// In a [MaterialApp], the edge glow color is the [ThemeData.accentColor]. /// 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 /// When building a [CustomScrollView] with a [GlowingOverscrollIndicator], the
/// indicator will apply to the entire scrollable area, regardless of what /// indicator will apply to the entire scrollable area, regardless of what
/// slivers the CustomScrollView contains. /// slivers the CustomScrollView contains.
/// ///
/// For example, if your CustomScrollView contains a SliverAppBar in the first /// For example, if your CustomScrollView contains a SliverAppBar in the first
/// position, the GlowingOverscrollIndicator will overlay the SliverAppBar. To /// position, the GlowingOverscrollIndicator will overlay the SliverAppBar. To
/// manipulate the position of the GlowingOverscrollIndicator in this case, use /// manipulate the position of the GlowingOverscrollIndicator in this case,
/// a [NestedScrollView]. /// 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} /// {@tool dartpad --template=stateless_widget_scaffold}
/// ///
...@@ -73,6 +112,13 @@ import 'ticker_provider.dart'; ...@@ -73,6 +112,13 @@ import 'ticker_provider.dart';
/// } /// }
/// ``` /// ```
/// {@end-tool} /// {@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 { class GlowingOverscrollIndicator extends StatefulWidget {
/// Creates a visual indication that a scroll view has overscrolled. /// Creates a visual indication that a scroll view has overscrolled.
/// ///
...@@ -199,6 +245,16 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator> ...@@ -199,6 +245,16 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
bool _handleScrollNotification(ScrollNotification notification) { bool _handleScrollNotification(ScrollNotification notification) {
if (!widget.notificationPredicate(notification)) if (!widget.notificationPredicate(notification))
return false; 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) { if (notification is OverscrollNotification) {
_GlowController controller; _GlowController controller;
if (notification.overscroll < 0.0) { if (notification.overscroll < 0.0) {
...@@ -213,6 +269,9 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator> ...@@ -213,6 +269,9 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
final OverscrollIndicatorNotification confirmationNotification = OverscrollIndicatorNotification(leading: isLeading); final OverscrollIndicatorNotification confirmationNotification = OverscrollIndicatorNotification(leading: isLeading);
confirmationNotification.dispatch(context); confirmationNotification.dispatch(context);
_accepted[isLeading] = confirmationNotification._accepted; _accepted[isLeading] = confirmationNotification._accepted;
if (_accepted[isLeading]) {
controller._paintOffset = confirmationNotification.paintOffset;
}
} }
assert(controller != null); assert(controller != null);
assert(notification.metrics.axis == widget.axis); assert(notification.metrics.axis == widget.axis);
...@@ -309,6 +368,8 @@ class _GlowController extends ChangeNotifier { ...@@ -309,6 +368,8 @@ class _GlowController extends ChangeNotifier {
_GlowState _state = _GlowState.idle; _GlowState _state = _GlowState.idle;
AnimationController _glowController; AnimationController _glowController;
Timer _pullRecedeTimer; Timer _pullRecedeTimer;
double _paintOffset = 0.0;
double _paintOffsetScrollPixels = 0.0;
// animation values // animation values
final Tween<double> _glowOpacityTween = Tween<double>(begin: 0.0, end: 0.0); final Tween<double> _glowOpacityTween = Tween<double>(begin: 0.0, end: 0.0);
...@@ -490,6 +551,7 @@ class _GlowController extends ChangeNotifier { ...@@ -490,6 +551,7 @@ class _GlowController extends ChangeNotifier {
final Offset center = Offset((size.width / 2.0) * (0.5 + _displacement), height - radius); final Offset center = Offset((size.width / 2.0) * (0.5 + _displacement), height - radius);
final Paint paint = Paint()..color = color.withOpacity(_glowOpacity.value); final Paint paint = Paint()..color = color.withOpacity(_glowOpacity.value);
canvas.save(); canvas.save();
canvas.translate(0.0, _paintOffset + _paintOffsetScrollPixels);
canvas.scale(1.0, scaleY); canvas.scale(1.0, scaleY);
canvas.clipRect(rect); canvas.clipRect(rect);
canvas.drawCircle(center, radius, paint); canvas.drawCircle(center, radius, paint);
...@@ -588,6 +650,18 @@ class OverscrollIndicatorNotification extends Notification with ViewportNotifica ...@@ -588,6 +650,18 @@ class OverscrollIndicatorNotification extends Notification with ViewportNotifica
/// view. /// view.
final bool leading; 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; bool _accepted = true;
/// Call this method if the glow should be prevented. /// Call this method if the glow should be prevented.
......
...@@ -320,6 +320,68 @@ void main() { ...@@ -320,6 +320,68 @@ void main() {
expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A0000FF))..saveRestore()); expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A0000FF))..saveRestore());
expect(painter, isNot(paints..circle()..circle())); 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 { class TestScrollBehavior1 extends ScrollBehavior {
......
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