diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart index d09185525ce..78c54c3d474 100644 --- a/packages/flutter/lib/src/material/stepper.dart +++ b/packages/flutter/lib/src/material/stepper.dart @@ -140,6 +140,7 @@ class Stepper extends StatefulWidget { this.onStepTapped, this.onStepContinue, this.onStepCancel, + this.controlsBuilder, }) : assert(steps != null), assert(type != null), assert(currentStep != null), @@ -174,6 +175,53 @@ class Stepper extends StatefulWidget { /// 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: [ + /// FlatButton( + /// onPressed: onStepContinue, + /// child: const Text('My Awesome Continue Message!'), + /// ), + /// FlatButton( + /// onPressed: onStepCancel, + /// child: const Text('My Awesome Cancel Message!'), + /// ), + /// ], + /// ), + /// }, + /// steps: const [ + /// 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 @@ class _StepperState extends State with TickerProviderStateMixin { } 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 b5e2ae0310e..0cebc9623df 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -3632,6 +3632,11 @@ typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index); /// [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 6b1e912902d..24eeeea242b 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -368,6 +368,91 @@ void main() { 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: [ + 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( + 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(