diff --git a/AUTHORS b/AUTHORS index f3edfbee3fc..433846e4e9a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -29,4 +29,5 @@ Lukasz Piliszczuk Felix Schmidt Artur Rymarz Stefan Mitev +Jasper van Riet Mattijs Fuijkschot diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index fc72709e089..ccc59c27a7e 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -548,6 +548,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.labelPadding, this.unselectedLabelColor, this.unselectedLabelStyle, + this.onTap, }) : assert(tabs != null), assert(isScrollable != null), assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)), @@ -660,6 +661,17 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// is null then the text style of the theme's body2 definition is used. final TextStyle unselectedLabelStyle; + /// An optional callback that's called when the [TabBar] is tapped. + /// + /// The callback is applied to the index of the tab where the tap occurred. + /// + /// This callback has no effect on the default handling of taps. It's for + /// applications that want to do a little extra work when a tab is tapped, + /// even if the tap doesn't change the TabController's index. TabBar [onTap] + /// callbacks should not make changes to the TabController since that would + /// interfere with the default tap handler. + final ValueChanged onTap; + /// A size whose height depends on if the tabs have both icons and text. /// /// [AppBar] uses this this size to compute its own preferred size. @@ -883,6 +895,9 @@ class _TabBarState extends State { void _handleTap(int index) { assert(index >= 0 && index < widget.tabs.length); _controller.animateTo(index); + if (widget.onTap != null) { + widget.onTap(index); + } } Widget _buildStyledTab(Widget child, bool selected, Animation animation) { diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 6dd33a17db8..9e4a8c324be 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -1760,6 +1760,87 @@ void main() { semantics.dispose(); }); + testWidgets('can be notified of TabBar onTap behavior', (WidgetTester tester) async { + int tabIndex = -1; + + Widget buildFrame({ + TabController controller, + List tabs, + }) { + return boilerplate( + child: Container( + child: TabBar( + controller: controller, + tabs: tabs.map((String tab) => Tab(text: tab)).toList(), + onTap: (int index) { + tabIndex = index; + }, + ), + ), + ); + } + + final List tabs = ['A', 'B', 'C']; + final TabController controller = TabController( + vsync: const TestVSync(), + length: tabs.length, + initialIndex: tabs.indexOf('C'), + ); + + await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller)); + expect(find.text('A'), findsOneWidget); + expect(find.text('B'), findsOneWidget); + expect(find.text('C'), findsOneWidget); + expect(controller, isNotNull); + expect(controller.index, 2); + expect(tabIndex, -1); // no tap so far so tabIndex should reflect that + + // Verify whether the [onTap] notification works when the [TabBar] animates. + + await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller)); + await tester.tap(find.text('B')); + await tester.pump(); + expect(controller.indexIsChanging, true); + await tester.pumpAndSettle(); + expect(controller.index, 1); + expect(controller.previousIndex, 2); + expect(controller.indexIsChanging, false); + expect(tabIndex, controller.index); + + tabIndex = -1; + + await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller)); + await tester.tap(find.text('C')); + await tester.pump(); + await tester.pumpAndSettle(); + expect(controller.index, 2); + expect(controller.previousIndex, 1); + expect(tabIndex, controller.index); + + tabIndex = -1; + + await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller)); + await tester.tap(find.text('A')); + await tester.pump(); + await tester.pumpAndSettle(); + expect(controller.index, 0); + expect(controller.previousIndex, 2); + expect(tabIndex, controller.index); + + tabIndex = -1; + + // Verify whether [onTap] is called even when the [TabController] does + // not change. + + final int currentControllerIndex = controller.index; + await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller)); + await tester.tap(find.text('A')); + await tester.pump(); + await tester.pumpAndSettle(); + expect(controller.index, currentControllerIndex); // controller has not changed + expect(tabIndex, 0); + }); + test('illegal constructor combinations', () { expect(() => Tab(icon: nonconst(null)), throwsAssertionError); expect(() => Tab(icon: Container(), text: 'foo', child: Container()), throwsAssertionError);