From c8e42b47f5ea8b5ff7bf2f2b0a2a8e765f1aa51d Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Mon, 26 Jun 2023 19:19:02 +0300 Subject: [PATCH] Fix `AnimatedList` & `AnimatedGrid` doesn't apply `MediaQuery` padding (#129556) fixes [AnimatedList does not take SafeArea into account when building the list ](https://github.com/flutter/flutter/issues/129539) ### Description This PR fixes an issue for `AnimatedList` & `AnimatedGrid` where `MediaQuery` padding isn't applied. See the [source](https://github.com/flutter/flutter/blob/a20db068dd9f72e2e4a35a3ce64f22d47b3d20f7/packages/flutter/lib/src/widgets/scroll_view.dart#L803-L833). While the `ListView` or `GridView` applies `MediaQuery` padding to its inner `SliverPadding`. This is missing from `AnimatedList` & `AnimatedGrid`. ![Digram of ListView applying MediaQuery padding](https://github.com/flutter/flutter/assets/48603081/01917900-cd26-4ca1-8e51-b7dcd1241471) The fix applies `MediaQuery` padding to the inner `SliverPadding` in `AnimatedList` & `AnimatedGrid`. ![Digram of AnimatedList & AnimatedGrid applying MediaQuery padding](https://github.com/flutter/flutter/assets/48603081/75d0a0ad-539c-485e-b3c1-770ee187086b) ### Code sample
expand to view the code sample ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData(useMaterial3: true), home: const Example(), ); } } class Example extends StatelessWidget { const Example({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Sample'), ), body: Row( children: [ Expanded( child: Column( children: [ const Text('ListView'), Expanded( child: ListView.builder( itemCount: 50, itemBuilder: (_, int index) { return ColoredBox( color: Theme.of(context).colorScheme.primaryContainer, child: Center( child: Text('$index', textAlign: TextAlign.center), ), ); }, ), ), ], ), ), const VerticalDivider(width: 4), Expanded( child: Column( children: [ const Text('AnimatedList'), Expanded( child: AnimatedList( initialItemCount: 50, itemBuilder: (_, int index, __) { return ColoredBox( color: Theme.of(context).colorScheme.primaryContainer, child: Center( child: Text('$index', textAlign: TextAlign.center), ), ); }, ), ), ], ), ), const VerticalDivider(width: 4), Expanded( child: Column( children: [ const Text('AnimatedGrid'), Expanded( child: AnimatedGrid( initialItemCount: 50, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, ), itemBuilder: (_, int index, __) { return ColoredBox( color: Theme.of(context).colorScheme.primaryContainer, child: Center( child: Text('$index', textAlign: TextAlign.center), ), ); }, ), ), ], )) ], ), ); } } ```
### Before ![Before preview image](https://github.com/flutter/flutter/assets/48603081/73954a8a-9d1d-4b9e-b6a3-cae8071f3462) ### After ![After preview image](https://github.com/flutter/flutter/assets/48603081/9f1dc48a-622f-4402-8d5e-8e6e3e150165) --- .../lib/src/widgets/animated_scroll_view.dart | 101 ++++++++++++++++-- .../test/widgets/animated_grid_test.dart | 39 +++++++ .../test/widgets/animated_list_test.dart | 36 +++++++ 3 files changed, 169 insertions(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/widgets/animated_scroll_view.dart b/packages/flutter/lib/src/widgets/animated_scroll_view.dart index a8555719030..85638313158 100644 --- a/packages/flutter/lib/src/widgets/animated_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/animated_scroll_view.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'basic.dart'; import 'framework.dart'; +import 'media_query.dart'; import 'scroll_controller.dart'; import 'scroll_delegate.dart'; import 'scroll_physics.dart'; @@ -30,6 +31,35 @@ import 'ticker_provider.dart'; /// ** See code in examples/api/lib/widgets/animated_list/animated_list.0.dart ** /// {@end-tool} /// +/// By default, [AnimatedList] will automatically pad the limits of the +/// list's scrollable to avoid partial obstructions indicated by +/// [MediaQuery]'s padding. To avoid this behavior, override with a +/// zero [padding] property. +/// +/// {@tool snippet} +/// The following example demonstrates how to override the default top and +/// bottom padding using [MediaQuery.removePadding]. +/// +/// ```dart +/// Widget myWidget(BuildContext context) { +/// return MediaQuery.removePadding( +/// context: context, +/// removeTop: true, +/// removeBottom: true, +/// child: AnimatedList( +/// initialItemCount: 50, +/// itemBuilder: (BuildContext context, int index, Animation animation) { +/// return Card( +/// color: Colors.amber, +/// child: Center(child: Text('$index')), +/// ); +/// } +/// ), +/// ); +/// } +/// ``` +/// {@end-tool} +/// /// See also: /// /// * [SliverAnimatedList], a sliver that animates items when they are inserted @@ -176,6 +206,7 @@ class AnimatedListState extends _AnimatedScrollViewState { itemBuilder: widget.itemBuilder, initialItemCount: widget.initialItemCount, ), + widget.scrollDirection, ); } } @@ -196,6 +227,38 @@ class AnimatedListState extends _AnimatedScrollViewState { /// ** See code in examples/api/lib/widgets/animated_grid/animated_grid.0.dart ** /// {@end-tool} /// +/// By default, [AnimatedGrid] will automatically pad the limits of the +/// grid's scrollable to avoid partial obstructions indicated by +/// [MediaQuery]'s padding. To avoid this behavior, override with a +/// zero [padding] property. +/// +/// {@tool snippet} +/// The following example demonstrates how to override the default top and +/// bottom padding using [MediaQuery.removePadding]. +/// +/// ```dart +/// Widget myWidget(BuildContext context) { +/// return MediaQuery.removePadding( +/// context: context, +/// removeTop: true, +/// removeBottom: true, +/// child: AnimatedGrid( +/// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( +/// crossAxisCount: 3, +/// ), +/// initialItemCount: 50, +/// itemBuilder: (BuildContext context, int index, Animation animation) { +/// return Card( +/// color: Colors.amber, +/// child: Center(child: Text('$index')), +/// ); +/// } +/// ), +/// ); +/// } +/// ``` +/// {@end-tool} +/// /// See also: /// /// * [SliverAnimatedGrid], a sliver which animates items when they are inserted @@ -353,6 +416,7 @@ class AnimatedGridState extends _AnimatedScrollViewState { itemBuilder: widget.itemBuilder, initialItemCount: widget.initialItemCount, ), + widget.scrollDirection, ); } } @@ -529,7 +593,35 @@ abstract class _AnimatedScrollViewState extends S _sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration); } - Widget _wrap(Widget sliver) { + Widget _wrap(Widget sliver, Axis direction) { + EdgeInsetsGeometry? effectivePadding = widget.padding; + if (widget.padding == null) { + final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context); + if (mediaQuery != null) { + // Automatically pad sliver with padding from MediaQuery. + final EdgeInsets mediaQueryHorizontalPadding = + mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0); + final EdgeInsets mediaQueryVerticalPadding = + mediaQuery.padding.copyWith(left: 0.0, right: 0.0); + // Consume the main axis padding with SliverPadding. + effectivePadding = direction == Axis.vertical + ? mediaQueryVerticalPadding + : mediaQueryHorizontalPadding; + // Leave behind the cross axis padding. + sliver = MediaQuery( + data: mediaQuery.copyWith( + padding: direction == Axis.vertical + ? mediaQueryHorizontalPadding + : mediaQueryVerticalPadding, + ), + child: sliver, + ); + } + } + + if (effectivePadding != null) { + sliver = SliverPadding(padding: effectivePadding, sliver: sliver); + } return CustomScrollView( scrollDirection: widget.scrollDirection, reverse: widget.reverse, @@ -538,12 +630,7 @@ abstract class _AnimatedScrollViewState extends S physics: widget.physics, clipBehavior: widget.clipBehavior, shrinkWrap: widget.shrinkWrap, - slivers: [ - SliverPadding( - padding: widget.padding ?? EdgeInsets.zero, - sliver: sliver, - ), - ], + slivers: [ sliver ], ); } } diff --git a/packages/flutter/test/widgets/animated_grid_test.dart b/packages/flutter/test/widgets/animated_grid_test.dart index 7a86f650813..dcec143a9e7 100644 --- a/packages/flutter/test/widgets/animated_grid_test.dart +++ b/packages/flutter/test/widgets/animated_grid_test.dart @@ -646,6 +646,45 @@ void main() { expect(tester.widget(find.byType(CustomScrollView)).clipBehavior, clipBehavior); }); + + testWidgets('AnimatedGrid applies MediaQuery padding', (WidgetTester tester) async { + const EdgeInsets padding = EdgeInsets.all(30.0); + EdgeInsets? innerMediaQueryPadding; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.all(30.0), + ), + child: AnimatedGrid( + initialItemCount: 6, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemBuilder: (BuildContext context, int index, Animation animation) { + innerMediaQueryPadding = MediaQuery.paddingOf(context); + return const Placeholder(); + }, + ), + ), + ), + ); + final Offset topLeft = tester.getTopLeft(find.byType(Placeholder).first); + // Automatically apply the top padding into sliver. + expect(topLeft, Offset(0.0, padding.top)); + + // Scroll to the bottom. + await tester.drag(find.byType(AnimatedGrid), const Offset(0.0, -1000.0)); + await tester.pumpAndSettle(); + + final Offset bottomRight = tester.getBottomRight(find.byType(Placeholder).last); + // Automatically apply the bottom padding into sliver. + expect(bottomRight, Offset(800.0, 600.0 - padding.bottom)); + + // Verify that the left/right padding is not applied. + expect(innerMediaQueryPadding, const EdgeInsets.symmetric(horizontal: 30.0)); + }); } class _StatefulListItem extends StatefulWidget { diff --git a/packages/flutter/test/widgets/animated_list_test.dart b/packages/flutter/test/widgets/animated_list_test.dart index 89a6ce6ac92..6e585c9fcd2 100644 --- a/packages/flutter/test/widgets/animated_list_test.dart +++ b/packages/flutter/test/widgets/animated_list_test.dart @@ -649,6 +649,42 @@ void main() { expect(tester.widget(find.byType(CustomScrollView)).shrinkWrap, true); }); + + testWidgets('AnimatedList applies MediaQuery padding', (WidgetTester tester) async { + const EdgeInsets padding = EdgeInsets.all(30.0); + EdgeInsets? innerMediaQueryPadding; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.all(30.0), + ), + child: AnimatedList( + initialItemCount: 3, + itemBuilder: (BuildContext context, int index, Animation animation) { + innerMediaQueryPadding = MediaQuery.paddingOf(context); + return const Placeholder(); + }, + ), + ), + ), + ); + final Offset topLeft = tester.getTopLeft(find.byType(Placeholder).first); + // Automatically apply the top padding into sliver. + expect(topLeft, Offset(0.0, padding.top)); + + // Scroll to the bottom. + await tester.drag(find.byType(AnimatedList), const Offset(0.0, -1000.0)); + await tester.pumpAndSettle(); + + final Offset bottomLeft = tester.getBottomLeft(find.byType(Placeholder).last); + // Automatically apply the bottom padding into sliver. + expect(bottomLeft, Offset(0.0, 600.0 - padding.bottom)); + + // Verify that the left/right padding is not applied. + expect(innerMediaQueryPadding, const EdgeInsets.symmetric(horizontal: 30.0)); + }); }