diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 58b34d5..55f498b 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -26,4 +26,7 @@ jobs: run: dart pub global activate pana - name: Analyze project source with pana - run: pana \ No newline at end of file + run: pana + + - name: Run flutter test + run: flutter test \ No newline at end of file diff --git a/lib/src/scroll_shadow.dart b/lib/src/scroll_shadow.dart index aa0df42..49f4aa2 100644 --- a/lib/src/scroll_shadow.dart +++ b/lib/src/scroll_shadow.dart @@ -191,7 +191,8 @@ class _ScrollShadowState extends State { reachedStart = metrics.pixels <= metrics.minScrollExtent; reachedEnd = metrics.pixels >= metrics.maxScrollExtent; _animate = true; - return false; + // Consume the notification to prevent possible ScrollShadow ancestor to interpret them a second timme + return true; } Axis? _axis; diff --git a/pubspec.lock b/pubspec.lock index 7eef894..7013c69 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,7 +34,7 @@ packages: source: hosted version: "1.1.1" collection: - dependency: transitive + dependency: "direct dev" description: name: collection sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 diff --git a/pubspec.yaml b/pubspec.yaml index e28dbd4..1071c6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,5 +18,6 @@ dev_dependencies: flutter_lints: ^2.0.2 flutter_test: sdk: flutter + collection: ^1.17.2 flutter: null \ No newline at end of file diff --git a/test/both_axis_test.dart b/test/both_axis_test.dart new file mode 100644 index 0000000..dade0ce --- /dev/null +++ b/test/both_axis_test.dart @@ -0,0 +1,157 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_scroll_shadow/flutter_scroll_shadow.dart'; + +const _verticalShadowSize = 40.0; +const _verticalShadowColor = Colors.pink; + +const _horizontalShadowSize = 15.0; +const _horizontalShadowColor = Colors.green; + +class _BothAxisShadow extends StatelessWidget { + const _BothAxisShadow(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + // Constraint the app size to be sure to have scroll shadows + child: SizedBox( + height: 300, + width: 300, + child: ScrollShadow( + size: _verticalShadowSize, + color: _verticalShadowColor, + child: ListView.builder(itemBuilder: (context, rowIndex) => _MyRow(rowIndex))), + ), + ), + )); + } +} + +class _MyRow extends StatelessWidget { + _MyRow(this.rowIndex); + + final int rowIndex; + final controller = ScrollController(); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + 'Row $rowIndex', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ScrollShadow( + size: _horizontalShadowSize, + color: _horizontalShadowColor, + child: Scrollbar( + scrollbarOrientation: ScrollbarOrientation.bottom, + thumbVisibility: true, + trackVisibility: true, + controller: controller, + child: SingleChildScrollView( + controller: controller, + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: List.generate( + 20, + (columnIndex) => Padding( + padding: const EdgeInsets.all(8.0), + child: Text('R${rowIndex}C$columnIndex'), + )), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +enum _Direction { + start, + end, +} + +AnimatedOpacity _findVerticalShadow(_Direction direction) { + final finder = find.byWidgetPredicate((widget) { + if (widget is! AnimatedOpacity) return false; + + final child = widget.child; + if (child is! Container) return false; + + if (child.constraints?.maxWidth != 40.0) return false; + + // From this point, we know that 'widget' is a shadow created by ScrollShadow + // Every following check throw instead of just returning false + + final decoration = child.decoration as BoxDecoration; + + final gradient = decoration.gradient as LinearGradient; + + expect(gradient.begin, Alignment.bottomCenter); + expect(gradient.end, Alignment.topCenter); + + if (const ListEquality() + .equals(gradient.colors, [_verticalShadowColor.withOpacity(0), _verticalShadowColor])) { + return direction == _Direction.start; + } else if (const ListEquality() + .equals(gradient.colors, [_verticalShadowColor, _verticalShadowColor.withOpacity(0)])) { + return direction == _Direction.end; + } else { + fail('The gradient colors should be one of the above'); + } + }); + final elementsFound = finder.evaluate(); + expect(elementsFound.length, 1); + return elementsFound.first.widget as AnimatedOpacity; +} + +void main() { + testWidgets('Double axis shadow', (tester) async { + await tester.pumpWidget(const _BothAxisShadow()); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // hitTestable guarantee the widget is visible, and not just in the widget tree + expect(find.text('Row 0').hitTestable(), findsOneWidget); + expect(find.text('R0C0').hitTestable(), findsOneWidget); + + // Only bottom shadow + expect(_findVerticalShadow(_Direction.start).opacity, 0.0); + expect(_findVerticalShadow(_Direction.end).opacity, 1.0); + + /// Scroll the first row to the right + await tester.fling(find.text('R0C0'), const Offset(-400, 0), 500); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.text('Row 0').hitTestable(), findsOneWidget); + expect(find.text('R0C0').hitTestable(), findsNothing); + + // Still only bottom shadow as we scroll the horizontal row + expect(_findVerticalShadow(_Direction.start).opacity, 0.0); + expect(_findVerticalShadow(_Direction.end).opacity, 1.0); + + /// Scroll downward + await tester.fling(find.text('Row 0'), const Offset(0, -100), 500); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.text('Row 0').hitTestable(), findsNothing); + expect(find.text('R0C0').hitTestable(), findsNothing); + + // Both top and bottom shadow + expect(_findVerticalShadow(_Direction.start).opacity, 1.0); + expect(_findVerticalShadow(_Direction.end).opacity, 1.0); + }); +}