Extended ButtonTheme, RoundedRectangleBorder, gallery buttons demo (#15723)

diff --git a/examples/flutter_gallery/lib/demo/material/buttons_demo.dart b/examples/flutter_gallery/lib/demo/material/buttons_demo.dart
index e63ac88..d68d7f1 100644
--- a/examples/flutter_gallery/lib/demo/material/buttons_demo.dart
+++ b/examples/flutter_gallery/lib/demo/material/buttons_demo.dart
@@ -53,25 +53,40 @@
 }
 
 class _ButtonsDemoState extends State<ButtonsDemo> {
+  ShapeBorder _buttonShape;
+
   @override
   Widget build(BuildContext context) {
+    final ButtonThemeData buttonTheme = ButtonTheme.of(context).copyWith(
+      shape: _buttonShape
+    );
+
     final List<ComponentDemoTabData> demos = <ComponentDemoTabData>[
       new ComponentDemoTabData(
         tabName: 'RAISED',
         description: _raisedText,
-        demoWidget: buildRaisedButton(),
+        demoWidget: new ButtonTheme.fromButtonThemeData(
+          data: buttonTheme,
+          child: buildRaisedButton(),
+        ),
         exampleCodeTag: _raisedCode,
       ),
       new ComponentDemoTabData(
         tabName: 'FLAT',
         description: _flatText,
-        demoWidget: buildFlatButton(),
+        demoWidget: new ButtonTheme.fromButtonThemeData(
+          data: buttonTheme,
+          child: buildFlatButton(),
+        ),
         exampleCodeTag: _flatCode,
       ),
       new ComponentDemoTabData(
         tabName: 'OUTLINE',
         description: _outlineText,
-        demoWidget: buildOutlineButton(),
+        demoWidget: new ButtonTheme.fromButtonThemeData(
+          data: buttonTheme,
+          child: buildOutlineButton(),
+        ),
         exampleCodeTag: _outlineCode,
       ),
       new ComponentDemoTabData(
@@ -97,6 +112,16 @@
     return new TabbedComponentDemoScaffold(
       title: 'Buttons',
       demos: demos,
+      actions: <Widget>[
+        new IconButton(
+          icon: const Icon(Icons.sentiment_very_satisfied),
+          onPressed: () {
+            setState(() {
+              _buttonShape = _buttonShape == null ? const StadiumBorder() : null;
+            });
+          },
+        ),
+      ],
     );
   }
 
diff --git a/examples/flutter_gallery/lib/demo/material/scrollable_tabs_demo.dart b/examples/flutter_gallery/lib/demo/material/scrollable_tabs_demo.dart
index 45e82ac..0c33e20 100644
--- a/examples/flutter_gallery/lib/demo/material/scrollable_tabs_demo.dart
+++ b/examples/flutter_gallery/lib/demo/material/scrollable_tabs_demo.dart
@@ -63,9 +63,9 @@
     });
   }
 
-  ShapeDecoration getIndicator() {
+  Decoration getIndicator() {
     if (!_customIndicator)
-      return null;
+      return const UnderlineTabIndicator();
 
     switch(_demoStyle) {
       case TabsDemoStyle.iconsAndText:
diff --git a/examples/flutter_gallery/lib/gallery/demo.dart b/examples/flutter_gallery/lib/gallery/demo.dart
index ff67cc9..ed8525f 100644
--- a/examples/flutter_gallery/lib/gallery/demo.dart
+++ b/examples/flutter_gallery/lib/gallery/demo.dart
@@ -35,11 +35,13 @@
 class TabbedComponentDemoScaffold extends StatelessWidget {
   const TabbedComponentDemoScaffold({
     this.title,
-    this.demos
+    this.demos,
+    this.actions,
   });
 
   final List<ComponentDemoTabData> demos;
   final String title;
+  final List<Widget> actions;
 
   void _showExampleCode(BuildContext context) {
     final String tag = demos[DefaultTabController.of(context).index].exampleCodeTag;
@@ -57,19 +59,21 @@
       child: new Scaffold(
         appBar: new AppBar(
           title: new Text(title),
-          actions: <Widget>[
-            new Builder(
-              builder: (BuildContext context) {
-                return new IconButton(
-                  icon: const Icon(Icons.description),
-                  tooltip: 'Show example code',
-                  onPressed: () {
-                    _showExampleCode(context);
-                  },
-                );
-              },
-            ),
-          ],
+          actions: (actions ?? <Widget>[])..addAll(
+            <Widget>[
+              new Builder(
+                builder: (BuildContext context) {
+                  return new IconButton(
+                    icon: const Icon(Icons.description),
+                    tooltip: 'Show example code',
+                    onPressed: () {
+                      _showExampleCode(context);
+                    },
+                  );
+                },
+              )
+            ],
+          ),
           bottom: new TabBar(
             isScrollable: true,
             tabs: demos.map((ComponentDemoTabData data) => new Tab(text: data.tabName)).toList(),
diff --git a/packages/flutter/lib/src/material/button_theme.dart b/packages/flutter/lib/src/material/button_theme.dart
index c59e4e2..3d9d74b 100644
--- a/packages/flutter/lib/src/material/button_theme.dart
+++ b/packages/flutter/lib/src/material/button_theme.dart
@@ -80,6 +80,16 @@
        ),
        super(key: key, child: child);
 
+  /// Creates a button theme from [data].
+  ///
+  /// The [data] argument must not be null.
+  const ButtonTheme.fromButtonThemeData({
+    Key key,
+    @required this.data,
+    Widget child,
+  }) : assert(data != null),
+       super(key: key, child: child);
+
   /// Creates a button theme that is appropriate for button bars, as used in
   /// dialog footers and in the headers of data tables.
   ///
@@ -248,6 +258,26 @@
   /// This property only affects [DropdownButton] and its menu.
   final bool alignedDropdown;
 
+  /// Creates a copy of this button theme data object with the matching fields
+  /// replaced with the non-null parameter values.
+  ButtonThemeData copyWith({
+    ButtonTextTheme textTheme,
+    double minWidth,
+    double height,
+    EdgeInsetsGeometry padding,
+    ShapeBorder shape,
+    bool alignedDropdown,
+  }) {
+    return new ButtonThemeData(
+      textTheme: textTheme ?? this.textTheme,
+      minWidth: minWidth ?? this.minWidth,
+      height: height ?? this.height,
+      padding: padding ?? this.padding,
+      shape: shape ?? this.shape,
+      alignedDropdown: alignedDropdown ?? this.alignedDropdown,
+    );
+  }
+
   @override
   bool operator ==(dynamic other) {
     if (other.runtimeType != runtimeType)
diff --git a/packages/flutter/lib/src/material/outline_button.dart b/packages/flutter/lib/src/material/outline_button.dart
index 2333892..4424d1e 100644
--- a/packages/flutter/lib/src/material/outline_button.dart
+++ b/packages/flutter/lib/src/material/outline_button.dart
@@ -454,7 +454,7 @@
     if (a is _OutlineBorder) {
       return new _OutlineBorder(
         side: BorderSide.lerp(a.side, side, t),
-        shape: shape.lerpFrom(a.shape, t),
+        shape: ShapeBorder.lerp(a.shape, shape, t),
       );
     }
     return super.lerpFrom(a, t);
@@ -466,7 +466,7 @@
     if (b is _OutlineBorder) {
       return new _OutlineBorder(
         side: BorderSide.lerp(side, b.side, t),
-        shape: shape.lerpTo(b.shape, t),
+        shape: ShapeBorder.lerp(shape, b.shape, t),
       );
     }
     return super.lerpTo(b, t);
diff --git a/packages/flutter/test/material/button_theme_test.dart b/packages/flutter/test/material/button_theme_test.dart
index 1d5829e..4cb3f0f 100644
--- a/packages/flutter/test/material/button_theme_test.dart
+++ b/packages/flutter/test/material/button_theme_test.dart
@@ -74,6 +74,31 @@
     expect(tester.getSize(find.byType(Material)), const Size(88.0, 36.0));
   });
 
+  test('ButtonThemeData.copyWith', () {
+    ButtonThemeData theme = const ButtonThemeData().copyWith();
+    expect(theme.textTheme, ButtonTextTheme.normal);
+    expect(theme.constraints, const BoxConstraints(minWidth: 88.0, minHeight: 36.0));
+    expect(theme.padding, const EdgeInsets.symmetric(horizontal: 16.0));
+    expect(theme.shape, const RoundedRectangleBorder(
+      borderRadius: const BorderRadius.all(const Radius.circular(2.0)),
+    ));
+    expect(theme.alignedDropdown, false);
+
+    theme = const ButtonThemeData().copyWith(
+      textTheme: ButtonTextTheme.primary,
+      minWidth: 100.0,
+      height: 200.0,
+      padding: EdgeInsets.zero,
+      shape: const StadiumBorder(),
+      alignedDropdown: true,
+    );
+    expect(theme.textTheme, ButtonTextTheme.primary);
+    expect(theme.constraints, const BoxConstraints(minWidth: 100.0, minHeight: 200.0));
+    expect(theme.padding, EdgeInsets.zero);
+    expect(theme.shape, const StadiumBorder());
+    expect(theme.alignedDropdown, true);
+  });
+
   testWidgets('Theme buttonTheme defaults', (WidgetTester tester) async {
     final ThemeData lightTheme = new ThemeData.light();
     ButtonTextTheme textTheme;