diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6fa5706a..65bd4d26 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -21,9 +21,11 @@ jobs: with: cache: true - run: dart run build_runner build --delete-conflicting-outputs - - uses: ZebraDevs/flutter-code-quality@v1.0.8 + - uses: ZebraDevs/flutter-code-quality@main with: - token: ${{secrets.GITHUB_TOKEN}} + token: ${{secrets.GITHUB_TOKEN}} + coverage-pass-score: '1' + check-secret: runs-on: ubuntu-latest diff --git a/example/lib/pages/components/top_app_bar_example.dart b/example/lib/pages/components/top_app_bar_example.dart index 7cf3f5cf..2837ae13 100644 --- a/example/lib/pages/components/top_app_bar_example.dart +++ b/example/lib/pages/components/top_app_bar_example.dart @@ -43,7 +43,7 @@ class _TopAppBarExampleState extends State { return ExampleScaffold( name: TopAppBarExample.name, child: ColoredBox( - color: colors.surfaceSecondary, + color: colors.surfaceWarm, child: SingleChildScrollView( child: Column( children: [ @@ -57,7 +57,7 @@ class _TopAppBarExampleState extends State { children: [ ZetaAvatar(size: ZetaAvatarSize.xs, image: image), Padding( - padding: const EdgeInsets.only(left: ZetaSpacing.medium), + padding: EdgeInsets.only(left: ZetaSpacing.medium), child: Text("Title"), ), ], @@ -154,7 +154,7 @@ class _TopAppBarExampleState extends State { children: [ ZetaAvatar(size: ZetaAvatarSize.xs, image: image), Padding( - padding: const EdgeInsets.only(left: ZetaSpacing.medium), + padding: EdgeInsets.only(left: ZetaSpacing.medium), child: Text("Title"), ), ], @@ -178,9 +178,9 @@ class _TopAppBarExampleState extends State { child: Container( width: 800, height: 800, - color: Zeta.of(context).colors.surfaceSecondary, + color: Zeta.of(context).colors.surfaceSelectedHover, child: CustomPaint( - painter: Painter(colors: colors), + painter: Painter(zeta: Zeta.of(context)), size: Size(800, 800), ), ), @@ -220,9 +220,9 @@ class _TopAppBarExampleState extends State { child: Container( width: 800, height: 800, - color: Zeta.of(context).colors.surfaceSecondary, + color: Zeta.of(context).colors.surfaceSelectedHover, child: CustomPaint( - painter: Painter(colors: colors), + painter: Painter(zeta: Zeta.of(context)), size: Size(800, 800), ), ), @@ -239,9 +239,9 @@ class _TopAppBarExampleState extends State { } class Painter extends CustomPainter { - final ZetaColors colors; + final Zeta zeta; - Painter({super.repaint, required this.colors}); + Painter({super.repaint, required this.zeta}); @override void paint(Canvas canvas, Size size) { @@ -249,7 +249,7 @@ class Painter extends CustomPainter { var p1 = Offset(i, -10); var p2 = Offset(800 + i, 810); var paint = Paint() - ..color = colors.primary + ..color = zeta.colors.surfaceDefault ..strokeWidth = ZetaSpacing.minimum; canvas.drawLine(p1, p2, paint); } diff --git a/lib/src/components/top_app_bar/extended_top_app_bar.dart b/lib/src/components/top_app_bar/extended_top_app_bar.dart index decb0983..4398852d 100644 --- a/lib/src/components/top_app_bar/extended_top_app_bar.dart +++ b/lib/src/components/top_app_bar/extended_top_app_bar.dart @@ -2,15 +2,6 @@ import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; -const _searchBarOffsetTop = ZetaSpacing.minimum * 1.5; -const _searchBarOffsetRight = ZetaSpacing.minimum * 22; -const _maxExtent = ZetaSpacing.minimum * 26; -const _minExtent = ZetaSpacing.xl_9; -const _leftMin = ZetaSpacing.large; -const _leftMax = ZetaSpacingBase.x12_5; -const _topMin = ZetaSpacing.xl_1; -const _topMax = ZetaSpacing.minimum * 15; - /// Delegate for creating an extended app bar, that grows and shrinks when scrolling. /// {@category Components} class ZetaExtendedAppBarDelegate extends SliverPersistentHeaderDelegate { @@ -38,26 +29,41 @@ class ZetaExtendedAppBarDelegate extends SliverPersistentHeaderDelegate { /// If `ZetaTopAppBarType.extend` shrinks. Does not affect other types of app bar. final bool shrinks; + static const double _maxExtent = 104; + static const double _minExtent = 52; + @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + const searchBarOffsetTop = ZetaSpacing.minimum * 1.5; + const searchBarOffsetRight = ZetaSpacing.minimum * 22; + const maxExtent = ZetaSpacing.minimum * 26; + const leftMin = ZetaSpacing.large; + + const topMin = ZetaSpacing.xl_1; + const topMax = ZetaSpacing.minimum * 15; + + /// If there is no leading widget, the left margin should not change + /// If there is a leading widget, the left margin should be the same as the leading widget's width plus padding + final leftMax = leading == null ? leftMin : _minExtent + ZetaSpacing.small; + return ConstrainedBox( - constraints: const BoxConstraints(minHeight: ZetaSpacing.xl_9, maxHeight: _maxExtent), + constraints: const BoxConstraints(minHeight: ZetaSpacing.xl_9, maxHeight: maxExtent), child: ColoredBox( color: Zeta.of(context).colors.surfacePrimary, child: Stack( children: [ Positioned( top: shrinks - ? (_topMax + (-1 * shrinkOffset)).clamp( - _topMin - + ? (topMax + (-1 * shrinkOffset)).clamp( + topMin - (searchController != null && searchController!.isEnabled - ? _searchBarOffsetTop + ? searchBarOffsetTop : ZetaSpacing.none), - _topMax, + topMax, ) - : _topMax, - left: shrinks ? ((shrinkOffset / _maxExtent) * ZetaSpacingBase.x50).clamp(_leftMin, _leftMax) : _leftMin, - right: searchController != null && searchController!.isEnabled ? _searchBarOffsetRight : ZetaSpacing.none, + : topMax, + left: shrinks ? ((shrinkOffset / maxExtent) * ZetaSpacingBase.x50).clamp(leftMin, leftMax) : leftMin, + right: searchController != null && searchController!.isEnabled ? searchBarOffsetRight : ZetaSpacing.none, child: title, ), if (leading != null) Positioned(top: ZetaSpacing.medium, left: ZetaSpacing.small, child: leading!), diff --git a/test/src/components/top_app_bar/extended_top_app_bar_test.dart b/test/src/components/top_app_bar/extended_top_app_bar_test.dart new file mode 100644 index 00000000..379b2a14 --- /dev/null +++ b/test/src/components/top_app_bar/extended_top_app_bar_test.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; +import '../../../test_utils/tolerant_comparator.dart'; +import '../../../test_utils/utils.dart'; + +void main() { + setUpAll(() { + final testUri = Uri.parse(getCurrentPath('top_app_bar')); + goldenFileComparator = TolerantComparator(testUri, tolerance: 0.01); + }); + + testWidgets('ZetaExtendedAppBarDelegate builds correctly', (WidgetTester tester) async { + const title = Text('Title'); + final actions = [IconButton(icon: const Icon(Icons.search), onPressed: () {})]; + final leading = IconButton(icon: const Icon(Icons.menu), onPressed: () {}); + const boxKey = Key('box'); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return SizedBox( + child: CustomScrollView( + slivers: [ + ZetaTopAppBar.extended(leading: leading, title: title, actions: actions), + SliverToBoxAdapter( + child: Container( + width: 800, + height: 700, + color: Zeta.of(context).colors.surfaceSelectedHover, + key: boxKey, + ), + ), + ], + ), + ); + }, + ), + ), + ); + + final boxFinder = find.byKey(boxKey); + expect(boxFinder, findsOneWidget); + + await tester.drag(boxFinder.first, const Offset(0, -100)); + await tester.pumpAndSettle(); + + final appBarFinder = find.byType(ZetaTopAppBar); + expect(appBarFinder, findsOneWidget); + + final titleFinder = find.descendant(of: appBarFinder, matching: find.byWidget(title)); + expect(titleFinder, findsOneWidget); + + final actionsFinder = find.descendant(of: appBarFinder, matching: find.byWidget(actions[0])); + expect(actionsFinder, findsOneWidget); + + final leadingFinder = find.descendant(of: appBarFinder, matching: find.byWidget(leading)); + expect(leadingFinder, findsOneWidget); + + await expectLater( + appBarFinder, + matchesGoldenFile(join(getCurrentPath('top_app_bar'), 'extended_app_bar_shrinks.png')), + ); + }); + + testWidgets('ZetaExtendedAppBarDelegate shrinks correctly with padding', (WidgetTester tester) async { + const title = Text('Title'); + final actions = [IconButton(icon: const Icon(Icons.search), onPressed: () {})]; + final leading = IconButton(icon: const Icon(Icons.menu), onPressed: () {}); + const boxKey = Key('box'); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return SizedBox( + child: CustomScrollView( + slivers: [ + ZetaTopAppBar.extended(leading: leading, title: title, actions: actions), + SliverToBoxAdapter( + child: Container( + width: 800, + height: 700, + color: Zeta.of(context).colors.surfaceSelectedHover, + key: boxKey, + ), + ), + ], + ), + ); + }, + ), + ), + ); + + final boxFinder = find.byKey(boxKey); + expect(boxFinder, findsOneWidget); + + await tester.drag(boxFinder.first, const Offset(0, -100)); + await tester.pumpAndSettle(); + + final appBarFinder = find.byType(ZetaTopAppBar); + expect(appBarFinder, findsOneWidget); + + final positionedFinder = find.descendant(of: appBarFinder, matching: find.byType(Positioned)); + + final positionedWidget = tester.widget(positionedFinder.first); + expect(positionedWidget.left, 60); + }); + testWidgets('ZetaExtendedAppBarDelegate shrinks correctly with padding and no leading', (WidgetTester tester) async { + const title = Text('Title'); + final actions = [IconButton(icon: const Icon(Icons.search), onPressed: () {})]; + + const boxKey = Key('box'); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return SizedBox( + child: CustomScrollView( + slivers: [ + ZetaTopAppBar.extended(title: title, actions: actions), + SliverToBoxAdapter( + child: Container( + width: 800, + height: 700, + color: Zeta.of(context).colors.surfaceSelectedHover, + key: boxKey, + ), + ), + ], + ), + ); + }, + ), + ), + ); + + final boxFinder = find.byKey(boxKey); + expect(boxFinder, findsOneWidget); + + await tester.drag(boxFinder.first, const Offset(0, -100)); + await tester.pumpAndSettle(); + + final appBarFinder = find.byType(ZetaTopAppBar); + expect(appBarFinder, findsOneWidget); + + final positionedFinder = find.descendant(of: appBarFinder, matching: find.byType(Positioned)); + + final positionedWidget = tester.widget(positionedFinder.first); + expect(positionedWidget.left, 16); + + await expectLater( + appBarFinder, + matchesGoldenFile(join(getCurrentPath('top_app_bar'), 'extended_app_bar_shrinks_with_no_leading.png')), + ); + }); +} diff --git a/test/src/components/top_app_bar/golden/extended_app_bar_shrinks.png b/test/src/components/top_app_bar/golden/extended_app_bar_shrinks.png new file mode 100644 index 00000000..6a0c1237 Binary files /dev/null and b/test/src/components/top_app_bar/golden/extended_app_bar_shrinks.png differ diff --git a/test/src/components/top_app_bar/golden/extended_app_bar_shrinks_with_no_leading.png b/test/src/components/top_app_bar/golden/extended_app_bar_shrinks_with_no_leading.png new file mode 100644 index 00000000..87b9ce7d Binary files /dev/null and b/test/src/components/top_app_bar/golden/extended_app_bar_shrinks_with_no_leading.png differ