diff --git a/bin/internal/goldens.version b/bin/internal/goldens.version index eeff2cd1d4fa96ff9a4ec03b527d9df909b50dfd..929fabf41417f6f6407ae07d4b76fd42d1ed7525 100644 --- a/bin/internal/goldens.version +++ b/bin/internal/goldens.version @@ -1 +1 @@ -a7f6061b8171f6fc82b6f437d13079ee26189438 +b84f87078729d0af8380fd9826091d8bafe6fcc7 diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 59e820abfde58cdabc454d90a6e689e176b09477..82daf1de9529d58e3d9a014015db85a7f8d03888 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -88,6 +88,7 @@ export 'src/material/snack_bar.dart'; export 'src/material/stepper.dart'; export 'src/material/switch.dart'; export 'src/material/switch_list_tile.dart'; +export 'src/material/tab_bar_theme.dart'; export 'src/material/tab_controller.dart'; export 'src/material/tab_indicator.dart'; export 'src/material/tabs.dart'; diff --git a/packages/flutter/lib/src/material/tab_bar_theme.dart b/packages/flutter/lib/src/material/tab_bar_theme.dart new file mode 100644 index 0000000000000000000000000000000000000000..8a6455ba0826b5286789ef2bd535829eb11f59a6 --- /dev/null +++ b/packages/flutter/lib/src/material/tab_bar_theme.dart @@ -0,0 +1,94 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'tabs.dart'; + +/// Defines a theme for [TabBar] widgets. +/// +/// A tab bar theme describes the color of the tab label and the size/shape of +/// the [TabBar.indicator]. +/// +/// Descendant widgets obtain the current theme's [TabBarTheme] object using +/// `Theme.of(context).tabBarTheme`. +/// [ThemeData.tabBarTheme] can be customized by copying it (using +/// [TabBarTheme.copyWith]). +/// +/// See also: +/// +/// * [TabBar], a widget that displays a horizontal row of tabs. +/// * [ThemeData], which describes the overall theme information for the +/// application. +class TabBarTheme extends Diagnosticable { + /// Creates a tab bar theme that can be used with [ThemeData.tabBarTheme]. + const TabBarTheme({ + this.indicator, + this.indicatorSize, + this.labelColor, + this.unselectedLabelColor, + }); + + /// Default value for [TabBar.indicator]. + final Decoration indicator; + + /// Default value for [TabBar.indicatorSize]. + final TabBarIndicatorSize indicatorSize; + + /// Default value for [TabBar.labelColor]. + final Color labelColor; + + /// Default value for [TabBar.unselectedLabelColor]. + final Color unselectedLabelColor; + + /// Creates a copy of this object but with the given fields replaced with the + /// new values. + TabBarTheme copyWith({ + Decoration indicator, + TabBarIndicatorSize indicatorSize, + Color labelColor, + Color unselectedLabelColor, + }) { + return TabBarTheme( + indicator: indicator ?? this.indicator, + indicatorSize: indicatorSize ?? this.indicatorSize, + labelColor: labelColor ?? this.labelColor, + unselectedLabelColor: unselectedLabelColor ?? this.unselectedLabelColor + ); + } + + /// Linearly interpolate between two tab bar themes. + /// + /// {@macro flutter.material.themeData.lerp} + static TabBarTheme lerp(TabBarTheme a, TabBarTheme b, double t) { + assert(a != null); + assert(b != null); + assert(t != null); + return TabBarTheme( + indicator: Decoration.lerp(a.indicator, b.indicator, t), + indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize, + labelColor: Color.lerp(a.labelColor, b.labelColor, t), + unselectedLabelColor: Color.lerp(a.unselectedLabelColor, b.unselectedLabelColor, t) + ); + } + + @override + int get hashCode { + return hashValues(indicator, indicatorSize, labelColor, unselectedLabelColor); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final TabBarTheme typedOther = other; + return typedOther.indicator == indicator + && typedOther.indicatorSize == indicatorSize + && typedOther.labelColor == labelColor + && typedOther.unselectedLabelColor == unselectedLabelColor; + } +} diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index a24503eeafe24bf3184051d51fdfe2443bbdd773..0669315479d337302a48fe8ca8b95be0d0eef9e5 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -15,6 +15,7 @@ import 'debug.dart'; import 'ink_well.dart'; import 'material.dart'; import 'material_localizations.dart'; +import 'tab_bar_theme.dart'; import 'tab_controller.dart'; import 'tab_indicator.dart'; import 'theme.dart'; @@ -149,14 +150,22 @@ class _TabStyle extends AnimatedWidget { @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); + final TabBarTheme tabBarTheme = themeData.tabBarTheme; + final TextStyle defaultStyle = labelStyle ?? themeData.primaryTextTheme.body2; final TextStyle defaultUnselectedStyle = unselectedLabelStyle ?? labelStyle ?? themeData.primaryTextTheme.body2; final Animation<double> animation = listenable; final TextStyle textStyle = selected ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value) : TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value); - final Color selectedColor = labelColor ?? themeData.primaryTextTheme.body2.color; - final Color unselectedColor = unselectedLabelColor ?? selectedColor.withAlpha(0xB2); // 70% alpha + final Color selectedColor = + labelColor + ?? tabBarTheme.labelColor + ?? themeData.primaryTextTheme.body2.color; + final Color unselectedColor = + unselectedLabelColor + ?? tabBarTheme.unselectedLabelColor + ?? selectedColor.withAlpha(0xB2); // 70% alpha final Color color = selected ? Color.lerp(selectedColor, unselectedColor, animation.value) : Color.lerp(unselectedColor, selectedColor, animation.value); @@ -504,6 +513,8 @@ class _TabBarScrollController extends ScrollController { /// /// Requires one of its ancestors to be a [Material] widget. /// +/// Uses values from [ThemeData.tabBarTheme] if it is set in the current context. +/// /// See also: /// /// * [TabBarView], which displays page views that correspond to each tab. @@ -687,8 +698,11 @@ class _TabBarState extends State<TabBar> { Decoration get _indicator { if (widget.indicator != null) return widget.indicator; + final ThemeData themeData = Theme.of(context); + if (themeData.tabBarTheme.indicator != null) + return themeData.tabBarTheme.indicator; - Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor; + Color color = widget.indicatorColor ?? themeData.indicatorColor; // ThemeData tries to avoid this by having indicatorColor avoid being the // primaryColor. However, it's possible that the tab bar is on a // Material that isn't the primaryColor. In that case, if the indicator @@ -741,7 +755,7 @@ class _TabBarState extends State<TabBar> { _indicatorPainter = _controller == null ? null : _IndicatorPainter( controller: _controller, indicator: _indicator, - indicatorSize: widget.indicatorSize, + indicatorSize: widget.indicatorSize ?? Theme.of(context).tabBarTheme.indicatorSize, tabKeys: _tabKeys, old: _indicatorPainter, ); diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 0cf3cc64a75a706d624bbfa6f20365fac33ebb65..9b2937007d18ba7f11d0e6ba9618a54af257b783 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -16,6 +16,7 @@ import 'ink_well.dart' show InteractiveInkFeatureFactory; import 'input_decorator.dart'; import 'page_transitions_theme.dart'; import 'slider_theme.dart'; +import 'tab_bar_theme.dart'; import 'typography.dart'; export 'package:flutter/services.dart' show Brightness; @@ -141,6 +142,7 @@ class ThemeData extends Diagnosticable { IconThemeData primaryIconTheme, IconThemeData accentIconTheme, SliderThemeData sliderTheme, + TabBarTheme tabBarTheme, ChipThemeData chipTheme, TargetPlatform platform, MaterialTapTargetSize materialTapTargetSize, @@ -185,6 +187,7 @@ class ThemeData extends Diagnosticable { errorColor ??= Colors.red[700]; inputDecorationTheme ??= const InputDecorationTheme(); pageTransitionsTheme ??= const PageTransitionsTheme(); + tabBarTheme ??= const TabBarTheme(); primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black); iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black87); @@ -207,6 +210,7 @@ class ThemeData extends Diagnosticable { primaryColorDark: primaryColorDark, valueIndicatorTextStyle: accentTextTheme.body2, ); + tabBarTheme ??= const TabBarTheme(); chipTheme ??= ChipThemeData.fromDefaults( secondaryColor: primaryColor, brightness: brightness, @@ -252,6 +256,7 @@ class ThemeData extends Diagnosticable { primaryIconTheme: primaryIconTheme, accentIconTheme: accentIconTheme, sliderTheme: sliderTheme, + tabBarTheme: tabBarTheme, chipTheme: chipTheme, platform: platform, materialTapTargetSize: materialTapTargetSize, @@ -307,6 +312,7 @@ class ThemeData extends Diagnosticable { @required this.primaryIconTheme, @required this.accentIconTheme, @required this.sliderTheme, + @required this.tabBarTheme, @required this.chipTheme, @required this.platform, @required this.materialTapTargetSize, @@ -348,6 +354,7 @@ class ThemeData extends Diagnosticable { assert(primaryIconTheme != null), assert(accentIconTheme != null), assert(sliderTheme != null), + assert(tabBarTheme != null), assert(chipTheme != null), assert(platform != null), assert(materialTapTargetSize != null), @@ -533,6 +540,9 @@ class ThemeData extends Diagnosticable { /// This is the value returned from [SliderTheme.of]. final SliderThemeData sliderTheme; + /// A theme for customizing the size, shape, and color of the tab bar indicator. + final TabBarTheme tabBarTheme; + /// The colors and styles used to render [Chip], [ /// /// This is the value returned from [ChipTheme.of]. @@ -600,6 +610,7 @@ class ThemeData extends Diagnosticable { IconThemeData primaryIconTheme, IconThemeData accentIconTheme, SliderThemeData sliderTheme, + TabBarTheme tabBarTheme, ChipThemeData chipTheme, TargetPlatform platform, MaterialTapTargetSize materialTapTargetSize, @@ -644,6 +655,7 @@ class ThemeData extends Diagnosticable { primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme, accentIconTheme: accentIconTheme ?? this.accentIconTheme, sliderTheme: sliderTheme ?? this.sliderTheme, + tabBarTheme: tabBarTheme ?? this.tabBarTheme, chipTheme: chipTheme ?? this.chipTheme, platform: platform ?? this.platform, materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize, @@ -718,6 +730,7 @@ class ThemeData extends Diagnosticable { /// Linearly interpolate between two themes. /// + /// {@template flutter.material.themeData.lerp} /// The arguments must not be null. /// /// The `t` argument represents position on the timeline, with 0.0 meaning @@ -731,6 +744,7 @@ class ThemeData extends Diagnosticable { /// /// Values for `t` are usually obtained from an [Animation<double>], such as /// an [AnimationController]. + /// {@endtemplate} static ThemeData lerp(ThemeData a, ThemeData b, double t) { assert(a != null); assert(b != null); @@ -774,6 +788,7 @@ class ThemeData extends Diagnosticable { primaryIconTheme: IconThemeData.lerp(a.primaryIconTheme, b.primaryIconTheme, t), accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, t), sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t), + tabBarTheme: TabBarTheme.lerp(a.tabBarTheme, b.tabBarTheme, t), chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t), platform: t < 0.5 ? a.platform : b.platform, materialTapTargetSize: t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize, @@ -827,6 +842,7 @@ class ThemeData extends Diagnosticable { (otherData.primaryIconTheme == primaryIconTheme) && (otherData.accentIconTheme == accentIconTheme) && (otherData.sliderTheme == sliderTheme) && + (otherData.tabBarTheme == tabBarTheme) && (otherData.chipTheme == chipTheme) && (otherData.platform == platform) && (otherData.materialTapTargetSize == materialTapTargetSize) && @@ -883,6 +899,7 @@ class ThemeData extends Diagnosticable { platform, materialTapTargetSize, pageTransitionsTheme, + tabBarTheme, ), ), ); @@ -928,6 +945,7 @@ class ThemeData extends Diagnosticable { properties.add(DiagnosticsProperty<IconThemeData>('primaryIconTheme', primaryIconTheme)); properties.add(DiagnosticsProperty<IconThemeData>('accentIconTheme', accentIconTheme)); properties.add(DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme)); + properties.add(DiagnosticsProperty<TabBarTheme>('tabBarTheme', tabBarTheme)); properties.add(DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme)); properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize)); properties.add(DiagnosticsProperty<PageTransitionsTheme>('pageTransitionsTheme', pageTransitionsTheme)); diff --git a/packages/flutter/test/material/tab_bar_theme_test.dart b/packages/flutter/test/material/tab_bar_theme_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..d8dfbc5d598fd3e41c614e974de76fe838dd36b3 --- /dev/null +++ b/packages/flutter/test/material/tab_bar_theme_test.dart @@ -0,0 +1,126 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const String _tab1Text = 'tab 1'; +const String _tab2Text = 'tab 2'; +const String _tab3Text = 'tab 3'; + +final Key _painterKey = UniqueKey(); + +const List<Tab> _tabs = <Tab>[ + Tab(text: _tab1Text, icon: Icon(Icons.looks_one)), + Tab(text: _tab2Text, icon: Icon(Icons.looks_two)), + Tab(text: _tab3Text, icon: Icon(Icons.looks_3)), +]; + +Widget _buildTabBar({ List<Tab> tabs = _tabs }) { + final TabController _tabController = TabController(length: 3, vsync: const TestVSync()); + + return RepaintBoundary( + key: _painterKey, + child: TabBar(tabs: tabs, controller: _tabController), + ); +} + +Widget _withTheme(TabBarTheme theme) { + return MaterialApp( + theme: ThemeData(tabBarTheme: theme), + home: Scaffold(body: _buildTabBar()), + ); +} + +RenderParagraph _iconRenderObject(WidgetTester tester, IconData icon) { + return tester.renderObject<RenderParagraph>( + find.descendant(of: find.byIcon(icon), matching: find.byType(RichText))); +} + +void main() { + testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async { + const Color labelColor = Colors.black; + const TabBarTheme tabBarTheme = TabBarTheme(labelColor: labelColor); + + await tester.pumpWidget(_withTheme(tabBarTheme)); + + final RenderParagraph textRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text)); + expect(textRenderObject.text.style.color, equals(labelColor)); + final RenderParagraph iconRenderObject = _iconRenderObject(tester, Icons.looks_one); + expect(iconRenderObject.text.style.color, equals(labelColor)); + }); + + testWidgets('Tab bar theme overrides label color (unselected)', (WidgetTester tester) async { + const Color unselectedLabelColor = Colors.black; + const TabBarTheme tabBarTheme = TabBarTheme(unselectedLabelColor: unselectedLabelColor); + + await tester.pumpWidget(_withTheme(tabBarTheme)); + + final RenderParagraph textRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text)); + expect(textRenderObject.text.style.color, equals(unselectedLabelColor)); + final RenderParagraph iconRenderObject = _iconRenderObject(tester, Icons.looks_two); + expect(iconRenderObject.text.style.color, equals(unselectedLabelColor)); + }); + + testWidgets('Tab bar theme overrides tab indicator size (tab)', (WidgetTester tester) async { + const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.tab); + + await tester.pumpWidget(_withTheme(tabBarTheme)); + + await expectLater( + find.byKey(_painterKey), + matchesGoldenFile('tab_bar_theme.tab_indicator_size_tab.png'), + skip: !Platform.isLinux, + ); + }); + + testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async { + const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label); + + await tester.pumpWidget(_withTheme(tabBarTheme)); + + await expectLater( + find.byKey(_painterKey), + matchesGoldenFile('tab_bar_theme.tab_indicator_size_label.png'), + skip: !Platform.isLinux, + ); + }); + + testWidgets('Tab bar theme - custom tab indicator', (WidgetTester tester) async { + final TabBarTheme tabBarTheme = TabBarTheme( + indicator: BoxDecoration( + border: Border.all(color: Colors.black), + shape: BoxShape.rectangle, + ) + ); + + await tester.pumpWidget(_withTheme(tabBarTheme)); + + await expectLater( + find.byKey(_painterKey), + matchesGoldenFile('tab_bar_theme.custom_tab_indicator.png'), + skip: !Platform.isLinux, + ); + }); + + testWidgets('Tab bar theme - beveled rect indicator', (WidgetTester tester) async { + final TabBarTheme tabBarTheme = TabBarTheme( + indicator: ShapeDecoration( + shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(20.0)), + color: Colors.black + ), + ); + + await tester.pumpWidget(_withTheme(tabBarTheme)); + + await expectLater( + find.byKey(_painterKey), + matchesGoldenFile('tab_bar_theme.beveled_rect_indicator.png'), + skip: !Platform.isLinux, + ); + }); +} diff --git a/packages/flutter/test/material/theme_test.dart b/packages/flutter/test/material/theme_test.dart index 0bcd342286f0c6319b161b211653fd37b2d365f9..8ea5f38eb37e3f820925270a091bf4d02b841b42 100644 --- a/packages/flutter/test/material/theme_test.dart +++ b/packages/flutter/test/material/theme_test.dart @@ -11,7 +11,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('ThemeDataTween control test', () { final ThemeData light = ThemeData.light(); - final ThemeData dark = ThemeData.light(); + final ThemeData dark = ThemeData.dark(); final ThemeDataTween tween = ThemeDataTween(begin: light, end: dark); expect(tween.lerp(0.25), equals(ThemeData.lerp(light, dark, 0.25))); });