Adds custom control builder functionality to Stepper (#23310)

* Adds test and builder
diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart
index d091855..78c54c3 100644
--- a/packages/flutter/lib/src/material/stepper.dart
+++ b/packages/flutter/lib/src/material/stepper.dart
@@ -140,6 +140,7 @@
     this.onStepTapped,
     this.onStepContinue,
     this.onStepCancel,
+    this.controlsBuilder,
   }) : assert(steps != null),
        assert(type != null),
        assert(currentStep != null),
@@ -174,6 +175,53 @@
   /// If null, the 'cancel' button will be disabled.
   final VoidCallback onStepCancel;
 
+  /// The callback for creating custom controls.
+  ///
+  /// If null, the default controls from the current theme will be used.
+  ///
+  /// This callback which takes in a context and two functions,[onStepContinue]
+  /// and [onStepCancel]. These can be used to control the stepper.
+  ///
+  /// ## Sample Code:
+  /// Creates a stepper control with custom buttons.
+  ///
+  /// ```dart
+  /// Stepper(
+  ///   controlsBuilder:
+  ///     (BuildContext context, {VoidCallback onStepContinue, VoidCallback onStepCancel}) {
+  ///        return Row(
+  ///          children: <Widget>[
+  ///            FlatButton(
+  ///              onPressed: onStepContinue,
+  ///              child: const Text('My Awesome Continue Message!'),
+  ///            ),
+  ///            FlatButton(
+  ///              onPressed: onStepCancel,
+  ///              child: const Text('My Awesome Cancel Message!'),
+  ///            ),
+  ///          ],
+  ///        ),
+  ///     },
+  ///   steps: const <Step>[
+  ///     Step(
+  ///       title: Text('A'),
+  ///       content: SizedBox(
+  ///         width: 100.0,
+  ///         height: 100.0,
+  ///       ),
+  ///     ),
+  ///     Step(
+  ///       title: Text('B'),
+  ///       content: SizedBox(
+  ///         width: 100.0,
+  ///         height: 100.0,
+  ///       ),
+  ///     ),
+  ///   ],
+  /// )
+  /// ```
+  final ControlsWidgetBuilder controlsBuilder;
+
   @override
   _StepperState createState() => _StepperState();
 }
@@ -327,6 +375,9 @@
   }
 
   Widget _buildVerticalControls() {
+    if (widget.controlsBuilder != null)
+      return widget.controlsBuilder(context, onStepContinue: widget.onStepContinue, onStepCancel: widget.onStepCancel);
+
     Color cancelColor;
 
     switch (Theme.of(context).brightness) {
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index b5e2ae0..0cebc96 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -3632,6 +3632,11 @@
 /// [MaterialApp.builder].
 typedef TransitionBuilder = Widget Function(BuildContext context, Widget child);
 
+/// A Signiture for a function that creates a widget given [onStepContinue] and [onStepCancel].
+///
+/// Used by [Stepper.builder].
+typedef ControlsWidgetBuilder = Widget Function(BuildContext context, {VoidCallback onStepContinue, VoidCallback onStepCancel});
+
 /// An [Element] that composes other [Element]s.
 ///
 /// Rather than creating a [RenderObject] directly, a [ComponentElement] creates
diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart
index 6b1e912..24eeeea 100644
--- a/packages/flutter/test/material/stepper_test.dart
+++ b/packages/flutter/test/material/stepper_test.dart
@@ -368,6 +368,91 @@
     expect(find.text('2'), findsOneWidget);
   });
 
+  testWidgets('Stepper custom controls test', (WidgetTester tester) async {
+    bool continuePressed = false;
+    void setContinue() {
+      continuePressed = true;
+    }
+
+    bool canceledPressed = false;
+    void setCanceled() {
+      canceledPressed = true;
+    }
+
+    final ControlsWidgetBuilder builder =
+      (BuildContext context, {VoidCallback onStepContinue, VoidCallback onStepCancel}) {
+        return Container(
+          margin: const EdgeInsets.only(top: 16.0),
+          child: ConstrainedBox(
+            constraints: const BoxConstraints.tightFor(height: 48.0),
+            child: Row(
+              children: <Widget>[
+                FlatButton(
+                  onPressed: onStepContinue,
+                  color: Colors.blue,
+                  textColor: Colors.white,
+                  textTheme: ButtonTextTheme.normal,
+                  child: const Text('Let us continue!'),
+                ),
+                Container(
+                  margin: const EdgeInsetsDirectional.only(start: 8.0),
+                  child: FlatButton(
+                    onPressed: onStepCancel,
+                    textColor: Colors.red,
+                    textTheme: ButtonTextTheme.normal,
+                    child: const Text('Cancel This!'),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        );
+      };
+
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Center(
+          child: Material(
+            child: Stepper(
+              controlsBuilder: builder,
+              onStepCancel: setCanceled,
+              onStepContinue: setContinue,
+              steps: const <Step>[
+                Step(
+                  title: Text('A'),
+                  state: StepState.complete,
+                  content: SizedBox(
+                    width: 100.0,
+                    height: 100.0,
+                  ),
+                ),
+                Step(
+                  title: Text('B'),
+                  content: SizedBox(
+                    width: 100.0,
+                    height: 100.0,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+
+    // 2 because stepper creates a set of controls for each step
+    expect(find.text('Let us continue!'), findsNWidgets(2));
+    expect(find.text('Cancel This!'), findsNWidgets(2));
+
+    await tester.tap(find.text('Cancel This!').first);
+    await tester.pumpAndSettle();
+    await tester.tap(find.text('Let us continue!').first);
+    await tester.pumpAndSettle();
+
+    expect(canceledPressed, isTrue);
+    expect(continuePressed, isTrue);
+  });
+
   testWidgets('Stepper error test', (WidgetTester tester) async {
     await tester.pumpWidget(
       MaterialApp(