[Material] Add TabBarTheme  (#22012)

* Add tab bar theme.

* Add tab bar theme.

* Add tests, pass context to getters.

* update goldens from linux box

* update goldens from linux box

* Added new golden test, addressed comments

* override hashCode and == in TabBarTheme

* Fix comment typos

* Addressed Hans' comments.

* Formatting changes

* [TabBarTheme] Fixed spacing

* [TabBarTheme] Update goldens version to latest commit
diff --git a/bin/internal/goldens.version b/bin/internal/goldens.version
index eeff2cd..929fabf 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 59e820a..82daf1d 100644
--- a/packages/flutter/lib/material.dart
+++ b/packages/flutter/lib/material.dart
@@ -88,6 +88,7 @@
 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 0000000..8a6455b
--- /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 a24503e..0669315 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -15,6 +15,7 @@
 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 @@
   @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 @@
 ///
 /// 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 @@
   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 @@
     _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 0cf3cc6..9b29370 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -16,6 +16,7 @@
 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 @@
     IconThemeData primaryIconTheme,
     IconThemeData accentIconTheme,
     SliderThemeData sliderTheme,
+    TabBarTheme tabBarTheme,
     ChipThemeData chipTheme,
     TargetPlatform platform,
     MaterialTapTargetSize materialTapTargetSize,
@@ -185,6 +187,7 @@
     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 @@
       primaryColorDark: primaryColorDark,
       valueIndicatorTextStyle: accentTextTheme.body2,
     );
+    tabBarTheme ??= const TabBarTheme();
     chipTheme ??= ChipThemeData.fromDefaults(
       secondaryColor: primaryColor,
       brightness: brightness,
@@ -252,6 +256,7 @@
       primaryIconTheme: primaryIconTheme,
       accentIconTheme: accentIconTheme,
       sliderTheme: sliderTheme,
+      tabBarTheme: tabBarTheme,
       chipTheme: chipTheme,
       platform: platform,
       materialTapTargetSize: materialTapTargetSize,
@@ -307,6 +312,7 @@
     @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 @@
        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 @@
   /// 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 @@
     IconThemeData primaryIconTheme,
     IconThemeData accentIconTheme,
     SliderThemeData sliderTheme,
+    TabBarTheme tabBarTheme,
     ChipThemeData chipTheme,
     TargetPlatform platform,
     MaterialTapTargetSize materialTapTargetSize,
@@ -644,6 +655,7 @@
       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 @@
 
   /// 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 @@
   ///
   /// 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 @@
       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 @@
            (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 @@
           platform,
           materialTapTargetSize,
           pageTransitionsTheme,
+          tabBarTheme,
         ),
       ),
     );
@@ -928,6 +945,7 @@
     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 0000000..d8dfbc5
--- /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 0bcd342..8ea5f38 100644
--- a/packages/flutter/test/material/theme_test.dart
+++ b/packages/flutter/test/material/theme_test.dart
@@ -11,7 +11,7 @@
 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)));
   });