Skip to content

Commit c8e42b4

Browse files
authored
Fix AnimatedList & AnimatedGrid doesn't apply MediaQuery padding (#129556)
fixes [AnimatedList does not take SafeArea into account when building the list ](#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 <details> <summary>expand to view the code sample</summary> ```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: <Widget>[ Expanded( child: Column( children: <Widget>[ 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: <Widget>[ 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: <Widget>[ 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), ), ); }, ), ), ], )) ], ), ); } } ``` </details> ### 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)
1 parent bac0589 commit c8e42b4

File tree

3 files changed

+169
-7
lines changed

3 files changed

+169
-7
lines changed

packages/flutter/lib/src/widgets/animated_scroll_view.dart

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
66

77
import 'basic.dart';
88
import 'framework.dart';
9+
import 'media_query.dart';
910
import 'scroll_controller.dart';
1011
import 'scroll_delegate.dart';
1112
import 'scroll_physics.dart';
@@ -30,6 +31,35 @@ import 'ticker_provider.dart';
3031
/// ** See code in examples/api/lib/widgets/animated_list/animated_list.0.dart **
3132
/// {@end-tool}
3233
///
34+
/// By default, [AnimatedList] will automatically pad the limits of the
35+
/// list's scrollable to avoid partial obstructions indicated by
36+
/// [MediaQuery]'s padding. To avoid this behavior, override with a
37+
/// zero [padding] property.
38+
///
39+
/// {@tool snippet}
40+
/// The following example demonstrates how to override the default top and
41+
/// bottom padding using [MediaQuery.removePadding].
42+
///
43+
/// ```dart
44+
/// Widget myWidget(BuildContext context) {
45+
/// return MediaQuery.removePadding(
46+
/// context: context,
47+
/// removeTop: true,
48+
/// removeBottom: true,
49+
/// child: AnimatedList(
50+
/// initialItemCount: 50,
51+
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
52+
/// return Card(
53+
/// color: Colors.amber,
54+
/// child: Center(child: Text('$index')),
55+
/// );
56+
/// }
57+
/// ),
58+
/// );
59+
/// }
60+
/// ```
61+
/// {@end-tool}
62+
///
3363
/// See also:
3464
///
3565
/// * [SliverAnimatedList], a sliver that animates items when they are inserted
@@ -176,6 +206,7 @@ class AnimatedListState extends _AnimatedScrollViewState<AnimatedList> {
176206
itemBuilder: widget.itemBuilder,
177207
initialItemCount: widget.initialItemCount,
178208
),
209+
widget.scrollDirection,
179210
);
180211
}
181212
}
@@ -196,6 +227,38 @@ class AnimatedListState extends _AnimatedScrollViewState<AnimatedList> {
196227
/// ** See code in examples/api/lib/widgets/animated_grid/animated_grid.0.dart **
197228
/// {@end-tool}
198229
///
230+
/// By default, [AnimatedGrid] will automatically pad the limits of the
231+
/// grid's scrollable to avoid partial obstructions indicated by
232+
/// [MediaQuery]'s padding. To avoid this behavior, override with a
233+
/// zero [padding] property.
234+
///
235+
/// {@tool snippet}
236+
/// The following example demonstrates how to override the default top and
237+
/// bottom padding using [MediaQuery.removePadding].
238+
///
239+
/// ```dart
240+
/// Widget myWidget(BuildContext context) {
241+
/// return MediaQuery.removePadding(
242+
/// context: context,
243+
/// removeTop: true,
244+
/// removeBottom: true,
245+
/// child: AnimatedGrid(
246+
/// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
247+
/// crossAxisCount: 3,
248+
/// ),
249+
/// initialItemCount: 50,
250+
/// itemBuilder: (BuildContext context, int index, Animation<double> animation) {
251+
/// return Card(
252+
/// color: Colors.amber,
253+
/// child: Center(child: Text('$index')),
254+
/// );
255+
/// }
256+
/// ),
257+
/// );
258+
/// }
259+
/// ```
260+
/// {@end-tool}
261+
///
199262
/// See also:
200263
///
201264
/// * [SliverAnimatedGrid], a sliver which animates items when they are inserted
@@ -353,6 +416,7 @@ class AnimatedGridState extends _AnimatedScrollViewState<AnimatedGrid> {
353416
itemBuilder: widget.itemBuilder,
354417
initialItemCount: widget.initialItemCount,
355418
),
419+
widget.scrollDirection,
356420
);
357421
}
358422
}
@@ -529,7 +593,35 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S
529593
_sliverAnimatedMultiBoxKey.currentState!.removeAllItems(builder, duration: duration);
530594
}
531595

532-
Widget _wrap(Widget sliver) {
596+
Widget _wrap(Widget sliver, Axis direction) {
597+
EdgeInsetsGeometry? effectivePadding = widget.padding;
598+
if (widget.padding == null) {
599+
final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
600+
if (mediaQuery != null) {
601+
// Automatically pad sliver with padding from MediaQuery.
602+
final EdgeInsets mediaQueryHorizontalPadding =
603+
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
604+
final EdgeInsets mediaQueryVerticalPadding =
605+
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
606+
// Consume the main axis padding with SliverPadding.
607+
effectivePadding = direction == Axis.vertical
608+
? mediaQueryVerticalPadding
609+
: mediaQueryHorizontalPadding;
610+
// Leave behind the cross axis padding.
611+
sliver = MediaQuery(
612+
data: mediaQuery.copyWith(
613+
padding: direction == Axis.vertical
614+
? mediaQueryHorizontalPadding
615+
: mediaQueryVerticalPadding,
616+
),
617+
child: sliver,
618+
);
619+
}
620+
}
621+
622+
if (effectivePadding != null) {
623+
sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
624+
}
533625
return CustomScrollView(
534626
scrollDirection: widget.scrollDirection,
535627
reverse: widget.reverse,
@@ -538,12 +630,7 @@ abstract class _AnimatedScrollViewState<T extends _AnimatedScrollView> extends S
538630
physics: widget.physics,
539631
clipBehavior: widget.clipBehavior,
540632
shrinkWrap: widget.shrinkWrap,
541-
slivers: <Widget>[
542-
SliverPadding(
543-
padding: widget.padding ?? EdgeInsets.zero,
544-
sliver: sliver,
545-
),
546-
],
633+
slivers: <Widget>[ sliver ],
547634
);
548635
}
549636
}

packages/flutter/test/widgets/animated_grid_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,45 @@ void main() {
646646

647647
expect(tester.widget<CustomScrollView>(find.byType(CustomScrollView)).clipBehavior, clipBehavior);
648648
});
649+
650+
testWidgets('AnimatedGrid applies MediaQuery padding', (WidgetTester tester) async {
651+
const EdgeInsets padding = EdgeInsets.all(30.0);
652+
EdgeInsets? innerMediaQueryPadding;
653+
await tester.pumpWidget(
654+
Directionality(
655+
textDirection: TextDirection.ltr,
656+
child: MediaQuery(
657+
data: const MediaQueryData(
658+
padding: EdgeInsets.all(30.0),
659+
),
660+
child: AnimatedGrid(
661+
initialItemCount: 6,
662+
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
663+
crossAxisCount: 2,
664+
),
665+
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
666+
innerMediaQueryPadding = MediaQuery.paddingOf(context);
667+
return const Placeholder();
668+
},
669+
),
670+
),
671+
),
672+
);
673+
final Offset topLeft = tester.getTopLeft(find.byType(Placeholder).first);
674+
// Automatically apply the top padding into sliver.
675+
expect(topLeft, Offset(0.0, padding.top));
676+
677+
// Scroll to the bottom.
678+
await tester.drag(find.byType(AnimatedGrid), const Offset(0.0, -1000.0));
679+
await tester.pumpAndSettle();
680+
681+
final Offset bottomRight = tester.getBottomRight(find.byType(Placeholder).last);
682+
// Automatically apply the bottom padding into sliver.
683+
expect(bottomRight, Offset(800.0, 600.0 - padding.bottom));
684+
685+
// Verify that the left/right padding is not applied.
686+
expect(innerMediaQueryPadding, const EdgeInsets.symmetric(horizontal: 30.0));
687+
});
649688
}
650689

651690
class _StatefulListItem extends StatefulWidget {

packages/flutter/test/widgets/animated_list_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,42 @@ void main() {
649649

650650
expect(tester.widget<CustomScrollView>(find.byType(CustomScrollView)).shrinkWrap, true);
651651
});
652+
653+
testWidgets('AnimatedList applies MediaQuery padding', (WidgetTester tester) async {
654+
const EdgeInsets padding = EdgeInsets.all(30.0);
655+
EdgeInsets? innerMediaQueryPadding;
656+
await tester.pumpWidget(
657+
Directionality(
658+
textDirection: TextDirection.ltr,
659+
child: MediaQuery(
660+
data: const MediaQueryData(
661+
padding: EdgeInsets.all(30.0),
662+
),
663+
child: AnimatedList(
664+
initialItemCount: 3,
665+
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
666+
innerMediaQueryPadding = MediaQuery.paddingOf(context);
667+
return const Placeholder();
668+
},
669+
),
670+
),
671+
),
672+
);
673+
final Offset topLeft = tester.getTopLeft(find.byType(Placeholder).first);
674+
// Automatically apply the top padding into sliver.
675+
expect(topLeft, Offset(0.0, padding.top));
676+
677+
// Scroll to the bottom.
678+
await tester.drag(find.byType(AnimatedList), const Offset(0.0, -1000.0));
679+
await tester.pumpAndSettle();
680+
681+
final Offset bottomLeft = tester.getBottomLeft(find.byType(Placeholder).last);
682+
// Automatically apply the bottom padding into sliver.
683+
expect(bottomLeft, Offset(0.0, 600.0 - padding.bottom));
684+
685+
// Verify that the left/right padding is not applied.
686+
expect(innerMediaQueryPadding, const EdgeInsets.symmetric(horizontal: 30.0));
687+
});
652688
}
653689

654690

0 commit comments

Comments
 (0)