diff --git a/packages/flutter/lib/src/material/carousel.dart b/packages/flutter/lib/src/material/carousel.dart index ae5cdd51530..c8c18dd1b8f 100644 --- a/packages/flutter/lib/src/material/carousel.dart +++ b/packages/flutter/lib/src/material/carousel.dart @@ -134,6 +134,7 @@ class CarouselView extends StatefulWidget { this.backgroundColor, this.elevation, this.shape, + this.itemClipBehavior, this.overlayColor, this.itemSnapping = false, this.shrinkExtent = 0.0, @@ -194,6 +195,7 @@ class CarouselView extends StatefulWidget { this.backgroundColor, this.elevation, this.shape, + this.itemClipBehavior, this.overlayColor, this.itemSnapping = false, this.shrinkExtent = 0.0, @@ -230,6 +232,14 @@ class CarouselView extends StatefulWidget { /// of 28.0. final ShapeBorder? shape; + /// The clip behavior for each carousel item. + /// + /// The item content will be clipped (or not) according to this option. + /// Refer to the [Clip] enum for more details on the different clip options. + /// + /// Defaults to [Clip.antiAlias]. + final Clip? itemClipBehavior; + /// The highlight color to indicate the carousel items are in pressed, hovered /// or focused states. /// @@ -415,6 +425,8 @@ class _CarouselViewState extends State { widget.shape ?? carouselTheme.shape ?? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0))); + final Clip effectiveItemClipBehavior = + widget.itemClipBehavior ?? carouselTheme.itemClipBehavior ?? Clip.antiAlias; final WidgetStateProperty effectiveOverlayColor = widget.overlayColor ?? carouselTheme.overlayColor ?? @@ -453,7 +465,7 @@ class _CarouselViewState extends State { return Padding( padding: effectivePadding, child: Material( - clipBehavior: Clip.antiAlias, + clipBehavior: effectiveItemClipBehavior, color: effectiveBackgroundColor, elevation: effectiveElevation, shape: effectiveShape, diff --git a/packages/flutter/lib/src/material/carousel_theme.dart b/packages/flutter/lib/src/material/carousel_theme.dart index 20b80753872..ddcaa11e980 100644 --- a/packages/flutter/lib/src/material/carousel_theme.dart +++ b/packages/flutter/lib/src/material/carousel_theme.dart @@ -39,6 +39,7 @@ class CarouselViewThemeData with Diagnosticable { this.overlayColor, this.shape, this.padding, + this.itemClipBehavior, }); /// The amount of space to surround each carousel item with. @@ -63,6 +64,14 @@ class CarouselViewThemeData with Diagnosticable { /// Overrides the default value for [CarouselView.shape]. final OutlinedBorder? shape; + /// The clip behavior for each carousel item. + /// + /// The item content will be clipped (or not) according to this option. + /// Refer to the [Clip] enum for more details on the different clip options. + /// + /// Overrides the default value for [CarouselView.itemClipBehavior]. + final Clip? itemClipBehavior; + /// The highlight color to indicate the carousel items are in pressed, hovered /// or focused states. /// @@ -77,6 +86,7 @@ class CarouselViewThemeData with Diagnosticable { OutlinedBorder? shape, WidgetStateProperty? overlayColor, EdgeInsets? padding, + Clip? itemClipBehavior, }) { return CarouselViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, @@ -84,6 +94,7 @@ class CarouselViewThemeData with Diagnosticable { shape: shape ?? this.shape, overlayColor: overlayColor ?? this.overlayColor, padding: padding ?? this.padding, + itemClipBehavior: itemClipBehavior ?? this.itemClipBehavior, ); } @@ -105,11 +116,13 @@ class CarouselViewThemeData with Diagnosticable { Color.lerp, ), padding: EdgeInsets.lerp(a?.padding, b?.padding, t), + itemClipBehavior: t < 0.5 ? a?.itemClipBehavior : b?.itemClipBehavior, ); } @override - int get hashCode => Object.hash(backgroundColor, elevation, shape, overlayColor, padding); + int get hashCode => + Object.hash(backgroundColor, elevation, shape, overlayColor, padding, itemClipBehavior); @override bool operator ==(Object other) { @@ -124,7 +137,8 @@ class CarouselViewThemeData with Diagnosticable { other.elevation == elevation && other.shape == shape && other.overlayColor == overlayColor && - other.padding == padding; + other.padding == padding && + other.itemClipBehavior == itemClipBehavior; } @override @@ -141,6 +155,7 @@ class CarouselViewThemeData with Diagnosticable { ), ); properties.add(DiagnosticsProperty('padding', padding, defaultValue: null)); + properties.add(EnumProperty('itemClipBehavior', itemClipBehavior, defaultValue: null)); } } diff --git a/packages/flutter/test/material/carousel_test.dart b/packages/flutter/test/material/carousel_test.dart index ef0a0e9fc89..2c9d35bbc22 100644 --- a/packages/flutter/test/material/carousel_test.dart +++ b/packages/flutter/test/material/carousel_test.dart @@ -1890,6 +1890,154 @@ void main() { } }); }); + + group('CarouselView item clipBehavior', () { + testWidgets('CarouselView Item clipBehavior defaults to Clip.antiAlias', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CarouselView( + itemExtent: 350, + children: List.generate(3, (int index) { + return Text('Item $index'); + }), + ), + ), + ), + ); + + final Material material = tester.firstWidget( + find.ancestor(of: find.text('Item 0'), matching: find.byType(Material)), + ); + + expect(material.clipBehavior, Clip.antiAlias); + }); + + testWidgets('CarouselView.weighted Item clipBehavior defaults to Clip.antiAlias', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CarouselView.weighted( + flexWeights: const [1, 1, 1], + children: List.generate(3, (int index) { + return Text('Item $index'); + }), + ), + ), + ), + ); + + final Material material = tester.firstWidget( + find.ancestor(of: find.text('Item 0'), matching: find.byType(Material)), + ); + + expect(material.clipBehavior, Clip.antiAlias); + }); + + testWidgets('CarouselView Item clipBehavior respects theme', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + carouselViewTheme: const CarouselViewThemeData(itemClipBehavior: Clip.hardEdge), + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + body: CarouselView( + itemExtent: 350, + children: List.generate(3, (int index) { + return Text('Item $index'); + }), + ), + ), + ), + ); + + final Material material = tester.firstWidget( + find.ancestor(of: find.text('Item 0'), matching: find.byType(Material)), + ); + + expect(material.clipBehavior, Clip.hardEdge); + }); + + testWidgets('CarouselView.weighted item clipBehavior respects theme', ( + WidgetTester tester, + ) async { + final ThemeData theme = ThemeData( + carouselViewTheme: const CarouselViewThemeData(itemClipBehavior: Clip.hardEdge), + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + body: CarouselView.weighted( + flexWeights: const [1, 1, 1], + children: List.generate(3, (int index) { + return Text('Item $index'); + }), + ), + ), + ), + ); + + final Material material = tester.firstWidget( + find.ancestor(of: find.text('Item 0'), matching: find.byType(Material)), + ); + + expect(material.clipBehavior, Clip.hardEdge); + }); + }); + + testWidgets('CarouselView item clipBehavior respects custom itemClipBehavior', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CarouselView( + itemExtent: 350, + itemClipBehavior: Clip.hardEdge, + children: List.generate(3, (int index) { + return Text('Item $index'); + }), + ), + ), + ), + ); + + final Material material = tester.firstWidget( + find.ancestor(of: find.text('Item 0'), matching: find.byType(Material)), + ); + + expect(material.clipBehavior, Clip.hardEdge); + }); + + testWidgets('CarouselView.weighted item clipBehavior respects custom itemClipBehavior', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CarouselView.weighted( + flexWeights: const [1, 1, 1], + itemClipBehavior: Clip.hardEdge, + children: List.generate(3, (int index) { + return Text('Item $index'); + }), + ), + ), + ), + ); + + final Material material = tester.firstWidget( + find.ancestor(of: find.text('Item 0'), matching: find.byType(Material)), + ); + + expect(material.clipBehavior, Clip.hardEdge); + }); } Finder getItem(int index) { diff --git a/packages/flutter/test/material/carousel_theme_test.dart b/packages/flutter/test/material/carousel_theme_test.dart index 3f927fa8b92..c0b0234e551 100644 --- a/packages/flutter/test/material/carousel_theme_test.dart +++ b/packages/flutter/test/material/carousel_theme_test.dart @@ -22,6 +22,7 @@ void main() { expect(carouselViewTheme.overlayColor, null); expect(carouselViewTheme.padding, null); expect(carouselViewTheme.shape, null); + expect(carouselViewTheme.itemClipBehavior, null); }); testWidgets('Default CarouselViewThemeData debugFillProperties', (WidgetTester tester) async { @@ -44,6 +45,7 @@ void main() { padding: EdgeInsets.zero, shape: RoundedRectangleBorder(), overlayColor: MaterialStatePropertyAll(Colors.red), + itemClipBehavior: Clip.hardEdge, ).debugFillProperties(builder); final List description = builder.properties @@ -57,6 +59,7 @@ void main() { 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)', 'overlayColor: WidgetStatePropertyAll(${Colors.red})', 'padding: EdgeInsets.zero', + 'itemClipBehavior: hardEdge', ]); }); @@ -97,8 +100,8 @@ void main() { expect(material.elevation, carouselViewTheme.elevation); expect(material.shape, carouselViewTheme.shape); expect(material.borderRadius, null); - expect(material.clipBehavior, Clip.antiAlias); expect(inkWell.overlayColor, carouselViewTheme.overlayColor); + expect(material.clipBehavior, carouselViewTheme.itemClipBehavior); }); testWidgets('Widgets properties override theme', (WidgetTester tester) async { @@ -110,6 +113,8 @@ void main() { borderRadius: BorderRadius.all(Radius.circular(10)), ); const WidgetStateProperty overlayColor = MaterialStatePropertyAll(Colors.green); + const Clip itemClipBehavior = Clip.hardEdge; + await tester.pumpWidget( MaterialApp( theme: ThemeData(carouselViewTheme: carouselViewTheme), @@ -122,6 +127,7 @@ void main() { shape: shape, overlayColor: overlayColor, itemExtent: 100, + itemClipBehavior: itemClipBehavior, children: [SizedBox(width: 100, height: 100)], ), ), @@ -150,6 +156,7 @@ void main() { expect(material.elevation, elevation); expect(material.shape, shape); expect(inkWell.overlayColor, overlayColor); + expect(material.clipBehavior, Clip.hardEdge); }); testWidgets('CarouselViewTheme can override Theme.carouselViewTheme', ( @@ -160,6 +167,7 @@ void main() { const double globalElevation = 5.0; const EdgeInsets globalPadding = EdgeInsets.all(10.0); const OutlinedBorder globalShape = RoundedRectangleBorder(); + const Clip globalItemClipBehavior = Clip.hardEdge; const Color localBackgroundColor = Color(0xffff0000); const Color localOverlayColor = Color(0xffffffff); @@ -168,6 +176,7 @@ void main() { const OutlinedBorder localShape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10)), ); + const Clip localItemClipBehavior = Clip.antiAlias; await tester.pumpWidget( MaterialApp( @@ -178,6 +187,7 @@ void main() { elevation: globalElevation, padding: globalPadding, shape: globalShape, + itemClipBehavior: globalItemClipBehavior, ), ), home: const Scaffold( @@ -189,6 +199,7 @@ void main() { elevation: localElevation, padding: localPadding, shape: localShape, + itemClipBehavior: localItemClipBehavior, ), child: CarouselView( itemExtent: 100, @@ -220,6 +231,7 @@ void main() { expect(material.elevation, localElevation); expect(material.shape, localShape); expect(inkWell.overlayColor?.resolve({}), localOverlayColor); + expect(material.clipBehavior, localItemClipBehavior); }); } @@ -229,6 +241,7 @@ CarouselViewThemeData _carouselViewThemeData() { const EdgeInsets padding = EdgeInsets.all(10.0); const OutlinedBorder shape = RoundedRectangleBorder(); const WidgetStateProperty overlayColor = MaterialStatePropertyAll(Colors.red); + const Clip itemClipBehavior = Clip.hardEdge; return const CarouselViewThemeData( backgroundColor: backgroundColor, @@ -236,5 +249,6 @@ CarouselViewThemeData _carouselViewThemeData() { padding: padding, shape: shape, overlayColor: overlayColor, + itemClipBehavior: itemClipBehavior, ); }