diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index c6f74eb14e..3ab9d6c721 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -321,6 +321,7 @@ class _IndicatorPainter extends CustomPainter { required this.indicatorSize, required this.tabKeys, required _IndicatorPainter? old, + required this.indicatorPadding, }) : assert(controller != null), assert(indicator != null), super(repaint: controller.animation) { @@ -331,6 +332,7 @@ class _IndicatorPainter extends CustomPainter { final TabController controller; final Decoration indicator; final TabBarIndicatorSize? indicatorSize; + final EdgeInsetsGeometry indicatorPadding; final List tabKeys; // _currentTabOffsets and _currentTextDirection are set each time TabBar @@ -392,7 +394,16 @@ class _IndicatorPainter extends CustomPainter { tabRight -= delta; } - return Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height); + final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection); + final Rect rect = Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height); + + if (!(rect.size >= insets.collapsedSize)) { + throw FlutterError( + 'indicatorPadding insets should be less than Tab Size\n' + 'Rect Size : ${rect.size}, Insets: ${insets.toString()}' + ); + } + return insets.deflateRect(rect); } @override @@ -658,26 +669,22 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// this property is ignored. final double indicatorWeight; - /// The horizontal padding for the line that appears below the selected tab. + + /// Padding for indicator. + /// This property will now no longer be ignored even if indicator is declared + /// or provided by [TabBarTheme] /// /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align /// the indicator with the tab's text for [Tab] widgets and all but the /// shortest [Tab.text] values. /// - /// The [EdgeInsets.top] and [EdgeInsets.bottom] values of the - /// [indicatorPadding] are ignored. - /// /// The default value of [indicatorPadding] is [EdgeInsets.zero]. - /// - /// If [indicator] is specified or provided from [TabBarTheme], - /// this property is ignored. final EdgeInsetsGeometry indicatorPadding; /// Defines the appearance of the selected tab indicator. /// /// If [indicator] is specified or provided from [TabBarTheme], - /// the [indicatorColor], [indicatorWeight], and [indicatorPadding] - /// properties are ignored. + /// the [indicatorColor], and [indicatorWeight] properties are ignored. /// /// The default, underline-style, selected tab indicator can be defined with /// [UnderlineTabIndicator]. @@ -857,7 +864,6 @@ class _TabBarState extends State { color = Colors.white; return UnderlineTabIndicator( - insets: widget.indicatorPadding, borderSide: BorderSide( width: widget.indicatorWeight, color: color, @@ -905,6 +911,7 @@ class _TabBarState extends State { controller: _controller!, indicator: _indicator, indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize, + indicatorPadding: widget.indicatorPadding, tabKeys: _tabKeys, old: _indicatorPainter, ); diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index b78c2062d4..a0ed498667 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -1538,6 +1538,307 @@ void main() { )); }); + testWidgets('TabBar with custom indicator and indicatorPadding(LTR)', (WidgetTester tester) async { + const Color indicatorColor = Color(0xFF00FF00); + const double padTop = 10.0; + const double padBottom = 12.0; + const double padLeft = 8.0; + const double padRight = 4.0; + const Decoration indicator = BoxDecoration(color: indicatorColor); + + final List tabs = List.generate(4, (int index) { + return Tab(text: 'Tab $index'); + }); + + final TabController controller = TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicator: indicator, + indicatorPadding: + const EdgeInsets.fromLTRB(padLeft, padTop, padRight, padBottom), + controller: controller, + tabs: tabs, + ), + ), + ), + ); + + final RenderBox tabBarBox = + tester.firstRenderObject(find.byType(TabBar)); + expect(tabBarBox.size.height, 48.0); + // 48 = _kTabHeight(46) + indicatorWeight(2.0) ~default + + const double indicatorBottom = 48.0 - padBottom; + const double indicatorTop = padTop; + double indicatorLeft = padLeft; + double indicatorRight = 200.0 - padRight; + + expect(tabBarBox, paints..rect( + rect: Rect.fromLTRB( + indicatorLeft, + indicatorTop, + indicatorRight, + indicatorBottom, + ), + color: indicatorColor, + )); + + // Select tab 3 + controller.index = 3; + await tester.pumpAndSettle(); + + indicatorLeft = 600.0 + padLeft; + indicatorRight = 800.0 - padRight; + + expect(tabBarBox, paints..rect( + rect: Rect.fromLTRB( + indicatorLeft, + indicatorTop, + indicatorRight, + indicatorBottom, + ), + color: indicatorColor, + )); + }); + + testWidgets('TabBar with custom indicator and indicatorPadding (RTL)', (WidgetTester tester) async { + const Color indicatorColor = Color(0xFF00FF00); + const double padTop = 10.0; + const double padBottom = 12.0; + const double padLeft = 8.0; + const double padRight = 4.0; + const Decoration indicator = BoxDecoration(color: indicatorColor); + + final List tabs = List.generate(4, (int index) { + return Tab(text: 'Tab $index'); + }); + + final TabController controller = TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + textDirection: TextDirection.rtl, + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicator: indicator, + indicatorPadding: const EdgeInsets.fromLTRB(padLeft, padTop, padRight, padBottom), + controller: controller, + tabs: tabs, + ), + ), + ), + ); + + final RenderBox tabBarBox = + tester.firstRenderObject(find.byType(TabBar)); + expect(tabBarBox.size.height, 48.0); + // 48 = _kTabHeight(46) + indicatorWeight(2.0) ~default + expect(tabBarBox.size.width, 800.0); + const double indicatorBottom = 48.0 - padBottom; + const double indicatorTop = padTop; + double indicatorLeft = 600.0 + padLeft; + double indicatorRight = 800.0 - padRight; + + expect(tabBarBox, paints..rect( + rect: Rect.fromLTRB( + indicatorLeft, + indicatorTop, + indicatorRight, + indicatorBottom, + ), + color: indicatorColor, + )); + + // Select tab 3 + controller.index = 3; + await tester.pumpAndSettle(); + + indicatorLeft = padLeft; + indicatorRight = 200.0 - padRight; + + expect(tabBarBox,paints..rect( + rect: Rect.fromLTRB( + indicatorLeft, + indicatorTop, + indicatorRight, + indicatorBottom, + ), + color: indicatorColor, + )); + }); + + testWidgets('TabBar with custom indicator - directional indicatorPadding (LTR)', (WidgetTester tester) async { + final List tabs = [ + SizedBox(key: UniqueKey(), width: 130.0, height: 30.0), + SizedBox(key: UniqueKey(), width: 140.0, height: 40.0), + SizedBox(key: UniqueKey(), width: 150.0, height: 50.0), + ]; + const Color indicatorColor = Color(0xFF00FF00); + const double padTop = 10.0; + const double padBottom = 12.0; + const double padStart = 8.0; + const double padEnd = 4.0; + const Decoration indicator = BoxDecoration(color: indicatorColor); + const double indicatorWeight = 2.0; // the default + + final TabController controller = TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicator: indicator, + indicatorPadding: const EdgeInsetsDirectional.fromSTEB( + padStart, padTop, padEnd, padBottom), + isScrollable: true, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + + final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); + const double tabBarHeight = 50.0 + indicatorWeight; // 50 = max tab height + expect(tabBarBox.size.height, tabBarHeight); + + // Tab0 width = 130, height = 30 + double tabLeft = kTabLabelPadding.left; + double tabRight = tabLeft + 130.0; + double tabTop = (tabBarHeight - indicatorWeight - 30.0) / 2.0; + double tabBottom = tabTop + 30.0; + Rect tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[0].key!)), tabRect); + + // Tab1 width = 140, height = 40 + tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left; + tabRight = tabLeft + 140.0; + tabTop = (tabBarHeight - indicatorWeight - 40.0) / 2.0; + tabBottom = tabTop + 40.0; + tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[1].key!)), tabRect); + + // Tab2 width = 150, height = 50 + tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left; + tabRight = tabLeft + 150.0; + tabTop = (tabBarHeight - indicatorWeight - 50.0) / 2.0; + tabBottom = tabTop + 50.0; + tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[2].key!)), tabRect); + + // Tab 0 selected, indicator padding resolves to left: 8.0, right: 4.0 + const double indicatorLeft = padStart; + final double indicatorRight = 130.0 + kTabLabelPadding.horizontal - padEnd; + const double indicatorTop = padTop; + const double indicatorBottom = tabBarHeight - padBottom; + expect(tabBarBox, paints..rect( + rect: Rect.fromLTRB( + indicatorLeft, + indicatorTop, + indicatorRight, + indicatorBottom, + ), + color: indicatorColor, + )); + }); + + testWidgets('TabBar with custom indicator - directional indicatorPadding (RTL)', (WidgetTester tester) async { + final List tabs = [ + SizedBox(key: UniqueKey(), width: 130.0, height: 30.0), + SizedBox(key: UniqueKey(), width: 140.0, height: 40.0), + SizedBox(key: UniqueKey(), width: 150.0, height: 50.0), + ]; + const Color indicatorColor = Color(0xFF00FF00); + const double padTop = 10.0; + const double padBottom = 12.0; + const double padStart = 8.0; + const double padEnd = 4.0; + const Decoration indicator = BoxDecoration(color: indicatorColor); + const double indicatorWeight = 2.0; // the default + + final TabController controller = TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + textDirection: TextDirection.rtl, + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicator: indicator, + indicatorPadding: const EdgeInsetsDirectional.fromSTEB( + padStart, padTop, padEnd, padBottom), + isScrollable: true, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + + final RenderBox tabBarBox = + tester.firstRenderObject(find.byType(TabBar)); + const double tabBarHeight = 50.0 + indicatorWeight; // 50 = max tab height + expect(tabBarBox.size.height, tabBarHeight); + + // Tab2 width = 150, height = 50 + double tabLeft = kTabLabelPadding.left; + double tabRight = tabLeft + 150.0; + double tabTop = (tabBarHeight - indicatorWeight - 50.0) / 2.0; + double tabBottom = tabTop + 50.0; + Rect tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[2].key!)), tabRect); + + // Tab1 width = 140, height = 40 + tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left; + tabRight = tabLeft + 140.0; + tabTop = (tabBarHeight - indicatorWeight - 40.0) / 2.0; + tabBottom = tabTop + 40.0; + tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[1].key!)), tabRect); + + // Tab0 width = 130, height = 30 + tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left; + tabRight = tabLeft + 130.0; + tabTop = (tabBarHeight - indicatorWeight - 30.0) / 2.0; + tabBottom = tabTop + 30.0; + tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); + expect(tester.getRect(find.byKey(tabs[0].key!)), tabRect); + + // Tab 0 selected, indicator padding resolves to left: 4.0, right: 8.0 + final double indicatorLeft = tabLeft - kTabLabelPadding.left + padEnd; + final double indicatorRight = tabRight + kTabLabelPadding.left - padStart; + const double indicatorTop = padTop; + const double indicatorBottom = tabBarHeight - padBottom; + + expect(tabBarBox, paints..rect( + rect: Rect.fromLTRB( + indicatorLeft, + indicatorTop, + indicatorRight, + indicatorBottom, + ), + color: indicatorColor, + )); + }); + testWidgets('TabBar with labelPadding', (WidgetTester tester) async { const double indicatorWeight = 2.0; // default indicator weight const EdgeInsets labelPadding = EdgeInsets.only(left: 3.0, right: 7.0);