diff --git a/packages/flutter/lib/src/material/tab_bar_theme.dart b/packages/flutter/lib/src/material/tab_bar_theme.dart index 49a95fe5a8..ff442fea33 100644 --- a/packages/flutter/lib/src/material/tab_bar_theme.dart +++ b/packages/flutter/lib/src/material/tab_bar_theme.dart @@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'ink_well.dart'; +import 'material_state.dart'; import 'tabs.dart'; import 'theme.dart'; @@ -33,6 +35,8 @@ class TabBarTheme with Diagnosticable { this.labelStyle, this.unselectedLabelColor, this.unselectedLabelStyle, + this.overlayColor, + this.splashFactory, }); /// Default value for [TabBar.indicator]. @@ -60,6 +64,12 @@ class TabBarTheme with Diagnosticable { /// Default value for [TabBar.unselectedLabelStyle]. final TextStyle? unselectedLabelStyle; + /// Default value for [TabBar.overlayColor]. + final MaterialStateProperty? overlayColor; + + /// Default value for [TabBar.splashFactory]. + final InteractiveInkFeatureFactory? splashFactory; + /// Creates a copy of this object but with the given fields replaced with the /// new values. TabBarTheme copyWith({ @@ -70,6 +80,8 @@ class TabBarTheme with Diagnosticable { TextStyle? labelStyle, Color? unselectedLabelColor, TextStyle? unselectedLabelStyle, + MaterialStateProperty? overlayColor, + InteractiveInkFeatureFactory? splashFactory, }) { return TabBarTheme( indicator: indicator ?? this.indicator, @@ -79,6 +91,8 @@ class TabBarTheme with Diagnosticable { labelStyle: labelStyle ?? this.labelStyle, unselectedLabelColor: unselectedLabelColor ?? this.unselectedLabelColor, unselectedLabelStyle: unselectedLabelStyle ?? this.unselectedLabelStyle, + overlayColor: overlayColor ?? this.overlayColor, + splashFactory: splashFactory ?? this.splashFactory, ); } @@ -104,6 +118,8 @@ class TabBarTheme with Diagnosticable { labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t), unselectedLabelColor: Color.lerp(a.unselectedLabelColor, b.unselectedLabelColor, t), unselectedLabelStyle: TextStyle.lerp(a.unselectedLabelStyle, b.unselectedLabelStyle, t), + overlayColor: _LerpColors(a.overlayColor, b.overlayColor, t), + splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory, ); } @@ -117,6 +133,8 @@ class TabBarTheme with Diagnosticable { labelStyle, unselectedLabelColor, unselectedLabelStyle, + overlayColor, + splashFactory, ); } @@ -133,6 +151,42 @@ class TabBarTheme with Diagnosticable { && other.labelPadding == labelPadding && other.labelStyle == labelStyle && other.unselectedLabelColor == unselectedLabelColor - && other.unselectedLabelStyle == unselectedLabelStyle; + && other.unselectedLabelStyle == unselectedLabelStyle + && other.overlayColor == overlayColor + && other.splashFactory == splashFactory; + } +} + + +@immutable +class _LerpColors implements MaterialStateProperty { + const _LerpColors(this.a, this.b, this.t); + + final MaterialStateProperty? a; + final MaterialStateProperty? b; + final double t; + + @override + Color? resolve(Set states) { + final Color? resolvedA = a?.resolve(states); + final Color? resolvedB = b?.resolve(states); + return Color.lerp(resolvedA, resolvedB, t); + } + + @override + int get hashCode { + return hashValues(a, b, t); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + return other is _LerpColors + && other.a == a + && other.b == b + && other.t == t; } } diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 40281206e4..e1504a482c 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -643,6 +643,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.enableFeedback, this.onTap, this.physics, + this.splashFactory, }) : assert(tabs != null), assert(isScrollable != null), assert(dragStartBehavior != null), @@ -786,14 +787,11 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// [MaterialState.hovered], and [MaterialState.pressed]. /// /// [MaterialState.pressed] triggers a ripple (an ink splash), per - /// the current Material Design spec. The [overlayColor] doesn't map - /// a state to [InkResponse.highlightColor] because a separate highlight - /// is not used by the current design guidelines. See - /// https://material.io/design/interaction/states.html#pressed + /// the current Material Design spec. /// /// If the overlay color is null or resolves to null, then the default values - /// for [InkResponse.focusColor], [InkResponse.hoverColor], [InkResponse.splashColor] - /// will be used instead. + /// for [InkResponse.focusColor], [InkResponse.hoverColor], [InkResponse.splashColor], + /// and [InkResponse.highlightColor] will be used instead. final MaterialStateProperty? overlayColor; /// {@macro flutter.widgets.scrollable.dragStartBehavior} @@ -832,6 +830,25 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// Defaults to matching platform conventions. final ScrollPhysics? physics; + /// Creates the tab bar's [InkWell] splash factory, which defines + /// the appearance of "ink" splashes that occur in response to taps. + /// + /// Use [NoSplash.splashFactory] to defeat ink splash rendering. For example + /// to defeat both the splash and the hover/pressed overlay, but not the + /// keyboard focused overlay: + /// ```dart + /// TabBar( + /// splashFactory: NoSplash.splashFactory, + /// overlayColor: MaterialStateProperty.resolveWith( + /// (Set states) { + /// return states.contains(MaterialState.focused) ? null : Colors.transparent; + /// }, + /// ), + /// ... + /// ) + /// ``` + final InteractiveInkFeatureFactory? splashFactory; + /// A size whose height depends on if the tabs have both icons and text. /// /// [AppBar] uses this size to compute its own preferred size. @@ -1187,7 +1204,8 @@ class _TabBarState extends State { mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click, onTap: () { _handleTap(index); }, enableFeedback: widget.enableFeedback ?? true, - overlayColor: widget.overlayColor, + overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor, + splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory, child: Padding( padding: EdgeInsets.only(bottom: widget.indicatorWeight), child: Stack( diff --git a/packages/flutter/test/material/tab_bar_theme_test.dart b/packages/flutter/test/material/tab_bar_theme_test.dart index 319554c2ae..33fb1ab9f3 100644 --- a/packages/flutter/test/material/tab_bar_theme_test.dart +++ b/packages/flutter/test/material/tab_bar_theme_test.dart @@ -54,6 +54,21 @@ RenderParagraph _iconRenderObject(WidgetTester tester, IconData icon) { } void main() { + test('TabBarTheme copyWith, ==, hashCode, defaults', () { + expect(const TabBarTheme(), const TabBarTheme().copyWith()); + expect(const TabBarTheme().hashCode, const TabBarTheme().copyWith().hashCode); + + expect(const TabBarTheme().indicator, null); + expect(const TabBarTheme().indicatorSize, null); + expect(const TabBarTheme().labelColor, null); + expect(const TabBarTheme().labelPadding, null); + expect(const TabBarTheme().labelStyle, null); + expect(const TabBarTheme().unselectedLabelColor, null); + expect(const TabBarTheme().unselectedLabelStyle, null); + expect(const TabBarTheme().overlayColor, null); + expect(const TabBarTheme().splashFactory, null); + }); + testWidgets('Tab bar defaults - label style and selected/unselected label colors', (WidgetTester tester) async { // tests for the default label color and label styles when tabBarTheme and tabBar do not provide any await tester.pumpWidget(_withTheme(null)); diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 6691f61c25..3c98bb1281 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -4322,6 +4322,62 @@ void main() { expect(controller3.index, 2); expect(pageController.page, 2); }); + + testWidgets('TabBar InkWell splashFactory and overlayColor', (WidgetTester tester) async { + const InteractiveInkFeatureFactory splashFactory = NoSplash.splashFactory; + final MaterialStateProperty overlayColor = MaterialStateProperty.resolveWith( + (Set states) => Colors.transparent, + ); + + // TabBarTheme splashFactory and overlayColor + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.light().copyWith( + tabBarTheme: TabBarTheme( + splashFactory: splashFactory, + overlayColor: overlayColor, + )), + home: DefaultTabController( + length: 1, + child: Scaffold( + appBar: AppBar( + bottom: TabBar( + tabs: [ + Container(width: 100, height: 100, color: Colors.green), + ], + ), + ), + ), + ), + ), + ); + + expect(tester.widget(find.byType(InkWell)).splashFactory, splashFactory); + expect(tester.widget(find.byType(InkWell)).overlayColor, overlayColor); + + // TabBar splashFactory and overlayColor + await tester.pumpWidget( + MaterialApp( + home: DefaultTabController( + length: 1, + child: Scaffold( + appBar: AppBar( + bottom: TabBar( + splashFactory: splashFactory, + overlayColor: overlayColor, + tabs: [ + Container(width: 100, height: 100, color: Colors.green), + ], + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); // theme animation + expect(tester.widget(find.byType(InkWell)).splashFactory, splashFactory); + expect(tester.widget(find.byType(InkWell)).overlayColor, overlayColor); + }); } class KeepAliveInk extends StatefulWidget {