// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(<String>['reduced-test-set']) library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgetsWithLeakTracking('Material3 - Shadow effect is not doubled', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/123064 debugDisableShadows = false; const double elevation = 1; const Color shadowColor = Colors.black; await tester.pumpWidget( MaterialApp( theme: ThemeData.light(useMaterial3: true), home: const Scaffold( bottomNavigationBar: BottomAppBar( elevation: elevation, shadowColor: shadowColor, ), ), ), ); final Finder finder = find.byType(BottomAppBar); expect(finder, paints..shadow(color: shadowColor, elevation: elevation)); expect(finder, paintsExactlyCountTimes(#drawShadow, 1)); debugDisableShadows = true; }); testWidgetsWithLeakTracking('Material3 - Only one layer with `color` is painted', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/122667 const Color bottomAppBarColor = Colors.black45; await tester.pumpWidget( MaterialApp( theme: ThemeData.light(useMaterial3: true), home: const Scaffold( bottomNavigationBar: BottomAppBar( color: bottomAppBarColor, // Avoid getting a surface tint color, to keep the color check below simple elevation: 0, ), ), ), ); // There should be just one color layer, and with the specified color. final Finder finder = find.descendant( of: find.byType(BottomAppBar), matching: find.byWidgetPredicate((Widget widget) { // A color layer is probably a [PhysicalShape] or [PhysicalModel], // either used directly or backing a [Material] (one without // [MaterialType.transparency]). return widget is PhysicalShape || widget is PhysicalModel; }), ); final Widget widget = tester.widgetList(finder).single; if (widget is PhysicalShape) { expect(widget.color, bottomAppBarColor); } else if (widget is PhysicalModel) { expect(widget.color, bottomAppBarColor); } else { // Should be unreachable: compare with the finder. assert(false); } }); testWidgetsWithLeakTracking('No overlap with floating action button', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( floatingActionButton: FloatingActionButton( onPressed: null, ), bottomNavigationBar: ShapeListener( BottomAppBar( child: SizedBox(height: 100.0), ), ), ), ), ); final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener)); final RenderBox renderBox = tester.renderObject(find.byType(BottomAppBar)); final Path expectedPath = Path() ..addRect(Offset.zero & renderBox.size); final Path actualPath = shapeListenerState.cache.value; expect( actualPath, coversSameAreaAs( expectedPath, areaToCompare: (Offset.zero & renderBox.size).inflate(5.0), ), ); }); testWidgetsWithLeakTracking('Material2 - Custom shape', (WidgetTester tester) async { final Key key = UniqueKey(); Future<void> pump(FloatingActionButtonLocation location) async { await tester.pumpWidget( SizedBox( width: 200, height: 200, child: RepaintBoundary( key: key, child: MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { }, ), floatingActionButtonLocation: location, bottomNavigationBar: const BottomAppBar( shape: AutomaticNotchedShape( BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), ContinuousRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(30.0))), ), notchMargin: 10.0, color: Colors.green, child: SizedBox(height: 100.0), ), ), ), ), ), ); } await pump(FloatingActionButtonLocation.endDocked); await expectLater( find.byKey(key), matchesGoldenFile('m2_bottom_app_bar.custom_shape.1.png'), ); await pump(FloatingActionButtonLocation.centerDocked); await tester.pumpAndSettle(); await expectLater( find.byKey(key), matchesGoldenFile('m2_bottom_app_bar.custom_shape.2.png'), ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44572 testWidgetsWithLeakTracking('Material3 - Custom shape', (WidgetTester tester) async { final Key key = UniqueKey(); Future<void> pump(FloatingActionButtonLocation location) async { await tester.pumpWidget( SizedBox( width: 200, height: 200, child: RepaintBoundary( key: key, child: MaterialApp( theme: ThemeData(useMaterial3: true), home: Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { }, ), floatingActionButtonLocation: location, bottomNavigationBar: const BottomAppBar( shape: AutomaticNotchedShape( BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), ContinuousRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(30.0))), ), notchMargin: 10.0, color: Colors.green, child: SizedBox(height: 100.0), ), ), ), ), ), ); } await pump(FloatingActionButtonLocation.endDocked); await expectLater( find.byKey(key), matchesGoldenFile('m3_bottom_app_bar.custom_shape.1.png'), ); await pump(FloatingActionButtonLocation.centerDocked); await tester.pumpAndSettle(); await expectLater( find.byKey(key), matchesGoldenFile('m3_bottom_app_bar.custom_shape.2.png'), ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44572 testWidgetsWithLeakTracking('Custom Padding', (WidgetTester tester) async { const EdgeInsets customPadding = EdgeInsets.all(10); await tester.pumpWidget( MaterialApp( theme: ThemeData.from(colorScheme: const ColorScheme.light()), home: Builder( builder: (BuildContext context) { return const Scaffold( body: Align( alignment: Alignment.bottomCenter, child: BottomAppBar( padding: customPadding, child: ColoredBox( color: Colors.green, child: SizedBox(width: 300, height: 60), ), ), ), ); }, ), ), ); final BottomAppBar bottomAppBar = tester.widget(find.byType(BottomAppBar)); expect(bottomAppBar.padding, customPadding); final Rect babRect = tester.getRect(find.byType(BottomAppBar)); final Rect childRect = tester.getRect(find.byType(ColoredBox)); expect(childRect, const Rect.fromLTRB(250, 530, 550, 590)); expect(babRect, const Rect.fromLTRB(240, 520, 560, 600)); }); testWidgetsWithLeakTracking('Material2 - Color defaults to Theme.bottomAppBarColor', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Builder( builder: (BuildContext context) { return Theme( data: Theme.of(context).copyWith(bottomAppBarColor: const Color(0xffffff00)), child: const Scaffold( floatingActionButton: FloatingActionButton( onPressed: null, ), bottomNavigationBar: BottomAppBar(), ), ); }, ), ), ); final PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0)); expect(physicalShape.color, const Color(0xffffff00)); }); testWidgetsWithLeakTracking('Material2 - Color overrides theme color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Builder( builder: (BuildContext context) { return Theme( data: Theme.of(context).copyWith(bottomAppBarColor: const Color(0xffffff00)), child: const Scaffold( floatingActionButton: FloatingActionButton( onPressed: null, ), bottomNavigationBar: BottomAppBar( color: Color(0xff0000ff), ), ), ); }, ), ), ); final PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0)); final Material material = tester.widget(find.byType(Material).at(1)); expect(physicalShape.color, const Color(0xff0000ff)); expect(material.color, null); /* no value in Material 2. */ }); testWidgetsWithLeakTracking('Material3 - Color overrides theme color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.light(useMaterial3: true).copyWith( bottomAppBarColor: const Color(0xffffff00), ), home: Builder( builder: (BuildContext context) { return const Scaffold( floatingActionButton: FloatingActionButton( onPressed: null, ), bottomNavigationBar: BottomAppBar( color: Color(0xff0000ff), surfaceTintColor: Colors.transparent, ), ); }, ), ), ); final PhysicalShape physicalShape = tester.widget( find.descendant(of: find.byType(BottomAppBar), matching: find.byType(PhysicalShape))); expect(physicalShape.color, const Color(0xff0000ff)); }); testWidgetsWithLeakTracking('Material3 - Shadow color is transparent', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true, ), home: const Scaffold( floatingActionButton: FloatingActionButton( onPressed: null, ), bottomNavigationBar: BottomAppBar( color: Color(0xff0000ff), ), ), ) ); final PhysicalShape physicalShape = tester.widget( find.descendant(of: find.byType(BottomAppBar), matching: find.byType(PhysicalShape))); expect(physicalShape.shadowColor, Colors.transparent); }); testWidgetsWithLeakTracking('Material2 - Dark theme applies an elevation overlay color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.from(useMaterial3: false, colorScheme: const ColorScheme.dark()), home: Scaffold( bottomNavigationBar: BottomAppBar( color: const ColorScheme.dark().surface, ), ), ), ); final PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0)); // For the default dark theme the overlay color for elevation 8 is 0xFF2D2D2D expect(physicalShape.color, const Color(0xFF2D2D2D)); }); testWidgetsWithLeakTracking('Material3 - Dark theme applies an elevation overlay color', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.dark(); await tester.pumpWidget( MaterialApp( theme: ThemeData.from(useMaterial3: true, colorScheme: colorScheme), home: Scaffold( bottomNavigationBar: BottomAppBar( color: colorScheme.surface, ), ), ), ); final PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0)); const double elevation = 3.0; // Default for M3. final Color overlayColor = ElevationOverlay.applySurfaceTint(colorScheme.surface, colorScheme.surfaceTint, elevation); expect(physicalShape.color, overlayColor); }); // This is a regression test for a bug we had where toggling the notch on/off // would crash, as the shouldReclip method of ShapeBorderClipper or // _BottomAppBarClipper would try an illegal downcast. testWidgetsWithLeakTracking('toggle shape to null', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( bottomNavigationBar: BottomAppBar( shape: RectangularNotch(), ), ), ), ); await tester.pumpWidget( const MaterialApp( home: Scaffold( bottomNavigationBar: BottomAppBar(), ), ), ); await tester.pumpWidget( const MaterialApp( home: Scaffold( bottomNavigationBar: BottomAppBar( shape: RectangularNotch(), ), ), ), ); }); testWidgetsWithLeakTracking('no notch when notch param is null', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( bottomNavigationBar: ShapeListener(BottomAppBar()), floatingActionButton: FloatingActionButton( onPressed: null, child: Icon(Icons.add), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, ), ), ); final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener)); final RenderBox renderBox = tester.renderObject(find.byType(BottomAppBar)); final Path expectedPath = Path() ..addRect(Offset.zero & renderBox.size); final Path actualPath = shapeListenerState.cache.value; expect( actualPath, coversSameAreaAs( expectedPath, areaToCompare: (Offset.zero & renderBox.size).inflate(5.0), ), ); }); testWidgetsWithLeakTracking('notch no margin', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( bottomNavigationBar: ShapeListener( BottomAppBar( shape: RectangularNotch(), notchMargin: 0.0, child: SizedBox(height: 100.0), ), ), floatingActionButton: FloatingActionButton( onPressed: null, child: Icon(Icons.add), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, ), ), ); final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener)); final RenderBox babBox = tester.renderObject(find.byType(BottomAppBar)); final Size babSize = babBox.size; final RenderBox fabBox = tester.renderObject(find.byType(FloatingActionButton)); final Size fabSize = fabBox.size; final double fabLeft = (babSize.width / 2.0) - (fabSize.width / 2.0); final double fabRight = fabLeft + fabSize.width; final double fabBottom = fabSize.height / 2.0; final Path expectedPath = Path() ..moveTo(0.0, 0.0) ..lineTo(fabLeft, 0.0) ..lineTo(fabLeft, fabBottom) ..lineTo(fabRight, fabBottom) ..lineTo(fabRight, 0.0) ..lineTo(babSize.width, 0.0) ..lineTo(babSize.width, babSize.height) ..lineTo(0.0, babSize.height) ..close(); final Path actualPath = shapeListenerState.cache.value; expect( actualPath, coversSameAreaAs( expectedPath, areaToCompare: (Offset.zero & babSize).inflate(5.0), ), ); }); testWidgetsWithLeakTracking('notch with margin', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( bottomNavigationBar: ShapeListener( BottomAppBar( shape: RectangularNotch(), notchMargin: 6.0, child: SizedBox(height: 100.0), ), ), floatingActionButton: FloatingActionButton( onPressed: null, child: Icon(Icons.add), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, ), ), ); final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener)); final RenderBox babBox = tester.renderObject(find.byType(BottomAppBar)); final Size babSize = babBox.size; final RenderBox fabBox = tester.renderObject(find.byType(FloatingActionButton)); final Size fabSize = fabBox.size; final double fabLeft = (babSize.width / 2.0) - (fabSize.width / 2.0) - 6.0; final double fabRight = fabLeft + fabSize.width + 6.0; final double fabBottom = 6.0 + fabSize.height / 2.0; final Path expectedPath = Path() ..moveTo(0.0, 0.0) ..lineTo(fabLeft, 0.0) ..lineTo(fabLeft, fabBottom) ..lineTo(fabRight, fabBottom) ..lineTo(fabRight, 0.0) ..lineTo(babSize.width, 0.0) ..lineTo(babSize.width, babSize.height) ..lineTo(0.0, babSize.height) ..close(); final Path actualPath = shapeListenerState.cache.value; expect( actualPath, coversSameAreaAs( expectedPath, areaToCompare: (Offset.zero & babSize).inflate(5.0), ), ); }); testWidgetsWithLeakTracking('Material2 - Observes safe area', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: const MediaQuery( data: MediaQueryData( padding: EdgeInsets.all(50.0), ), child: Scaffold( bottomNavigationBar: BottomAppBar( child: Center( child: Text('safe'), ), ), ), ), ), ); expect( tester.getBottomLeft(find.widgetWithText(Center, 'safe')), const Offset(50.0, 550.0), ); }); testWidgetsWithLeakTracking('Material3 - Observes safe area', (WidgetTester tester) async { const double safeAreaPadding = 50.0; await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: true), home: const MediaQuery( data: MediaQueryData( padding: EdgeInsets.all(safeAreaPadding), ), child: Scaffold( bottomNavigationBar: BottomAppBar( child: Center( child: Text('safe'), ), ), ), ), ), ); const double appBarVerticalPadding = 12.0; const double appBarHorizontalPadding = 16.0; expect( tester.getBottomLeft(find.widgetWithText(Center, 'safe')), const Offset( safeAreaPadding + appBarHorizontalPadding, 600 - safeAreaPadding - appBarVerticalPadding, ), ); }); testWidgetsWithLeakTracking('clipBehavior is propagated', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( bottomNavigationBar: BottomAppBar( shape: RectangularNotch(), notchMargin: 0.0, child: SizedBox(height: 100.0), ), ), ), ); PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape)); expect(physicalShape.clipBehavior, Clip.none); await tester.pumpWidget( const MaterialApp( home: Scaffold( bottomNavigationBar: BottomAppBar( shape: RectangularNotch(), notchMargin: 0.0, clipBehavior: Clip.antiAliasWithSaveLayer, child: SizedBox(height: 100.0), ), ), ), ); physicalShape = tester.widget(find.byType(PhysicalShape)); expect(physicalShape.clipBehavior, Clip.antiAliasWithSaveLayer); }); testWidgetsWithLeakTracking('Material2 - BottomAppBar with shape when Scaffold.bottomNavigationBar == null', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/80878 final ThemeData theme = ThemeData(useMaterial3: false); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: FloatingActionButton( backgroundColor: Colors.green, child: const Icon(Icons.home), onPressed: () {}, ), body: Stack( children: <Widget>[ Container( color: Colors.amber, ), Container( alignment: Alignment.bottomCenter, child: BottomAppBar( color: Colors.green, shape: const CircularNotchedRectangle(), child: Container(height: 50), ), ), ], ), ), ), ); expect(tester.getRect(find.byType(FloatingActionButton)), const Rect.fromLTRB(372, 528, 428, 584)); expect(tester.getSize(find.byType(BottomAppBar)), const Size(800, 50)); }); testWidgetsWithLeakTracking('Material3 - BottomAppBar with shape when Scaffold.bottomNavigationBar == null', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/80878 final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget( MaterialApp( theme: theme, home: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: FloatingActionButton( backgroundColor: Colors.green, child: const Icon(Icons.home), onPressed: () {}, ), body: Stack( children: <Widget>[ Container( color: Colors.amber, ), Container( alignment: Alignment.bottomCenter, child: BottomAppBar( color: Colors.green, shape: const CircularNotchedRectangle(), child: Container(height: 50), ), ), ], ), ), ), ); expect(tester.getRect(find.byType(FloatingActionButton)), const Rect.fromLTRB(372, 528, 428, 584)); expect(tester.getSize(find.byType(BottomAppBar)), const Size(800, 80)); }); testWidgetsWithLeakTracking('notch with margin and top padding, home safe area', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/90024 await tester.pumpWidget( const MediaQuery( data: MediaQueryData( padding: EdgeInsets.only(top: 128), ), child: MaterialApp( useInheritedMediaQuery: true, home: SafeArea( child: Scaffold( bottomNavigationBar: ShapeListener( BottomAppBar( shape: RectangularNotch(), notchMargin: 6.0, child: SizedBox(height: 100.0), ), ), floatingActionButton: FloatingActionButton( onPressed: null, child: Icon(Icons.add), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, ), ), ), ), ); final ShapeListenerState shapeListenerState = tester.state(find.byType(ShapeListener)); final RenderBox babBox = tester.renderObject(find.byType(BottomAppBar)); final Size babSize = babBox.size; final RenderBox fabBox = tester.renderObject(find.byType(FloatingActionButton)); final Size fabSize = fabBox.size; final double fabLeft = (babSize.width / 2.0) - (fabSize.width / 2.0) - 6.0; final double fabRight = fabLeft + fabSize.width + 6.0; final double fabBottom = 6.0 + fabSize.height / 2.0; final Path expectedPath = Path() ..moveTo(0.0, 0.0) ..lineTo(fabLeft, 0.0) ..lineTo(fabLeft, fabBottom) ..lineTo(fabRight, fabBottom) ..lineTo(fabRight, 0.0) ..lineTo(babSize.width, 0.0) ..lineTo(babSize.width, babSize.height) ..lineTo(0.0, babSize.height) ..close(); final Path actualPath = shapeListenerState.cache.value; expect( actualPath, coversSameAreaAs( expectedPath, areaToCompare: (Offset.zero & babSize).inflate(5.0), ), ); }); testWidgetsWithLeakTracking('BottomAppBar does not apply custom clipper without FAB', (WidgetTester tester) async { Widget buildWidget({Widget? fab}) { return MaterialApp( home: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: fab, bottomNavigationBar: BottomAppBar( color: Colors.green, shape: const CircularNotchedRectangle(), child: Container(height: 50), ), ), ); } await tester.pumpWidget(buildWidget(fab: FloatingActionButton(onPressed: () { }))); PhysicalShape physicalShape = tester.widget(find.byType(PhysicalShape).at(0)); expect(physicalShape.clipper.toString(), '_BottomAppBarClipper'); await tester.pumpWidget(buildWidget()); physicalShape = tester.widget(find.byType(PhysicalShape).at(0)); expect(physicalShape.clipper.toString(), 'ShapeBorderClipper'); }); testWidgetsWithLeakTracking('Material3 - BottomAppBar adds bottom padding to height', (WidgetTester tester) async { const double bottomPadding = 35.0; await tester.pumpWidget( MediaQuery( data: const MediaQueryData( padding: EdgeInsets.only(bottom: bottomPadding), viewPadding: EdgeInsets.only(bottom: bottomPadding), ), child: MaterialApp( theme: ThemeData(useMaterial3: true), home: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.endContained, floatingActionButton: FloatingActionButton(onPressed: () { }), bottomNavigationBar: BottomAppBar( child: IconButton( icon: const Icon(Icons.search), onPressed: () {}, ), ), ), ), ) ); final Rect bottomAppBar = tester.getRect(find.byType(BottomAppBar)); final Rect iconButton = tester.getRect(find.widgetWithIcon(IconButton, Icons.search)); final Rect fab = tester.getRect(find.byType(FloatingActionButton)); // The height of the bottom app bar should be its height(default is 80.0) + bottom safe area height. expect(bottomAppBar.height, 80.0 + bottomPadding); // The vertical position of the icon button and fab should be center of the area excluding the bottom padding. final double barCenter = bottomAppBar.topLeft.dy + (bottomAppBar.height - bottomPadding) / 2; expect(iconButton.center.dy, barCenter); expect(fab.center.dy, barCenter); }); } // The bottom app bar clip path computation is only available at paint time. // In order to examine the notch path we implement this caching painter which // at paint time looks for a descendant PhysicalShape and caches the // clip path it is using. class ClipCachePainter extends CustomPainter { ClipCachePainter(this.context); late Path value; BuildContext context; @override void paint(Canvas canvas, Size size) { final RenderPhysicalShape physicalShape = findPhysicalShapeChild(context)!; value = physicalShape.clipper!.getClip(size); } RenderPhysicalShape? findPhysicalShapeChild(BuildContext context) { RenderPhysicalShape? result; context.visitChildElements((Element e) { final RenderObject renderObject = e.findRenderObject()!; if (renderObject.runtimeType == RenderPhysicalShape) { assert(result == null); result = renderObject as RenderPhysicalShape; } else { result = findPhysicalShapeChild(e); } }); return result; } @override bool shouldRepaint(ClipCachePainter oldDelegate) { return true; } } class ShapeListener extends StatefulWidget { const ShapeListener(this.child, { super.key }); final Widget child; @override State createState() => ShapeListenerState(); } class ShapeListenerState extends State<ShapeListener> { @override Widget build(BuildContext context) { return CustomPaint( painter: cache, child: widget.child, ); } late ClipCachePainter cache; @override void didChangeDependencies() { super.didChangeDependencies(); cache = ClipCachePainter(context); } } class RectangularNotch extends NotchedShape { const RectangularNotch(); @override Path getOuterPath(Rect host, Rect? guest) { if (guest == null) { return Path()..addRect(host); } return Path() ..moveTo(host.left, host.top) ..lineTo(guest.left, host.top) ..lineTo(guest.left, guest.bottom) ..lineTo(guest.right, guest.bottom) ..lineTo(guest.right, host.top) ..lineTo(host.right, host.top) ..lineTo(host.right, host.bottom) ..lineTo(host.left, host.bottom) ..close(); } }