diff --git a/packages/flutter/lib/src/material/floating_action_button.dart b/packages/flutter/lib/src/material/floating_action_button.dart index 0cfd990d68..14d07360c3 100644 --- a/packages/flutter/lib/src/material/floating_action_button.dart +++ b/packages/flutter/lib/src/material/floating_action_button.dart @@ -147,6 +147,7 @@ class FloatingActionButton extends StatelessWidget { this.autofocus = false, this.materialTapTargetSize, this.isExtended = false, + this.enableFeedback, }) : assert(elevation == null || elevation >= 0.0), assert(focusElevation == null || focusElevation >= 0.0), assert(hoverElevation == null || hoverElevation >= 0.0), @@ -189,6 +190,7 @@ class FloatingActionButton extends StatelessWidget { this.autofocus = false, Widget? icon, required Widget label, + this.enableFeedback, }) : assert(elevation == null || elevation >= 0.0), assert(focusElevation == null || focusElevation >= 0.0), assert(hoverElevation == null || hoverElevation >= 0.0), @@ -409,6 +411,19 @@ class FloatingActionButton extends StatelessWidget { /// * [MaterialTapTargetSize], for a description of how this affects tap targets. final MaterialTapTargetSize? materialTapTargetSize; + /// Whether detected gestures should provide acoustic and/or haptic feedback. + /// + /// For example, on Android a tap will produce a clicking sound and a + /// long-press will produce a short vibration, when feedback is enabled. + /// + /// If null, [FloatingActionButtonThemeData.enableFeedback] is used. + /// If both are null, then default value is true. + /// + /// See also: + /// + /// * [Feedback] for providing platform-specific feedback to certain actions. + final bool? enableFeedback; + final BoxConstraints _sizeConstraints; static const double _defaultElevation = 6; @@ -455,6 +470,8 @@ class FloatingActionButton extends StatelessWidget { ?? _defaultHighlightElevation; final MaterialTapTargetSize materialTapTargetSize = this.materialTapTargetSize ?? theme.materialTapTargetSize; + final bool enableFeedback = this.enableFeedback + ?? floatingActionButtonTheme.enableFeedback ?? true; final TextStyle textStyle = theme.textTheme.button!.copyWith( color: foregroundColor, letterSpacing: 1.2, @@ -483,6 +500,7 @@ class FloatingActionButton extends StatelessWidget { focusNode: focusNode, autofocus: autofocus, child: child, + enableFeedback: enableFeedback, ); if (tooltip != null) { diff --git a/packages/flutter/lib/src/material/floating_action_button_theme.dart b/packages/flutter/lib/src/material/floating_action_button_theme.dart index 8bf7da1362..625194bb2a 100644 --- a/packages/flutter/lib/src/material/floating_action_button_theme.dart +++ b/packages/flutter/lib/src/material/floating_action_button_theme.dart @@ -42,6 +42,7 @@ class FloatingActionButtonThemeData with Diagnosticable { this.disabledElevation, this.highlightElevation, this.shape, + this.enableFeedback, }); /// Color to be used for the unselected, enabled [FloatingActionButton]'s @@ -89,6 +90,12 @@ class FloatingActionButtonThemeData with Diagnosticable { /// The shape to be used for the floating action button's [Material]. final ShapeBorder? shape; + /// If specified, defines the feedback property for [FloatingActionButton]. + /// + /// If [FloatingActionButton.enableFeedback] is provided, [enableFeedback] is + /// ignored. + final bool? enableFeedback; + /// Creates a copy of this object with the given fields replaced with the /// new values. FloatingActionButtonThemeData copyWith({ @@ -103,6 +110,7 @@ class FloatingActionButtonThemeData with Diagnosticable { double? disabledElevation, double? highlightElevation, ShapeBorder? shape, + bool? enableFeedback, }) { return FloatingActionButtonThemeData( foregroundColor: foregroundColor ?? this.foregroundColor, @@ -116,6 +124,7 @@ class FloatingActionButtonThemeData with Diagnosticable { disabledElevation: disabledElevation ?? this.disabledElevation, highlightElevation: highlightElevation ?? this.highlightElevation, shape: shape ?? this.shape, + enableFeedback: enableFeedback ?? this.enableFeedback, ); } @@ -140,6 +149,7 @@ class FloatingActionButtonThemeData with Diagnosticable { disabledElevation: lerpDouble(a?.disabledElevation, b?.disabledElevation, t), highlightElevation: lerpDouble(a?.highlightElevation, b?.highlightElevation, t), shape: ShapeBorder.lerp(a?.shape, b?.shape, t), + enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, ); } @@ -157,6 +167,7 @@ class FloatingActionButtonThemeData with Diagnosticable { disabledElevation, highlightElevation, shape, + enableFeedback, ); } @@ -177,7 +188,8 @@ class FloatingActionButtonThemeData with Diagnosticable { && other.hoverElevation == hoverElevation && other.disabledElevation == disabledElevation && other.highlightElevation == highlightElevation - && other.shape == shape; + && other.shape == shape + && other.enableFeedback == enableFeedback; } @override @@ -196,5 +208,6 @@ class FloatingActionButtonThemeData with Diagnosticable { properties.add(DoubleProperty('disabledElevation', disabledElevation, defaultValue: defaultData.disabledElevation)); properties.add(DoubleProperty('highlightElevation', highlightElevation, defaultValue: defaultData.highlightElevation)); properties.add(DiagnosticsProperty('shape', shape, defaultValue: defaultData.shape)); + properties.add(DiagnosticsProperty('enableFeedback', enableFeedback, defaultValue: defaultData.enableFeedback)); } } diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index 4a07d57049..73af88d38f 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart @@ -11,6 +11,7 @@ import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; +import 'feedback_tester.dart'; void main() { testWidgets('Floating Action Button control test', (WidgetTester tester) async { @@ -963,6 +964,116 @@ void main() { expect(find.byKey(iconKey), findsOneWidget); expect(find.byKey(labelKey), findsNothing); }); + + group('feedback', () { + late FeedbackTester feedback; + + setUp(() { + feedback = FeedbackTester(); + }); + + tearDown(() { + feedback.dispose(); + }); + + testWidgets('FloatingActionButton with enabled feedback', (WidgetTester tester) async { + const bool enableFeedback = true; + + await tester.pumpWidget(MaterialApp( + home: FloatingActionButton( + onPressed: () {}, + enableFeedback: enableFeedback, + child: const Icon(Icons.access_alarm), + ), + )); + + await tester.tap(find.byType(RawMaterialButton)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 1); + expect(feedback.hapticCount, 0); + }); + + testWidgets('FloatingActionButton with disabled feedback', (WidgetTester tester) async { + const bool enableFeedback = false; + + await tester.pumpWidget(MaterialApp( + home: FloatingActionButton( + onPressed: () {}, + enableFeedback: enableFeedback, + child: const Icon(Icons.access_alarm), + ), + )); + + await tester.tap(find.byType(RawMaterialButton)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 0); + expect(feedback.hapticCount, 0); + }); + + testWidgets('FloatingActionButton with enabled feedback by default', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: FloatingActionButton( + onPressed: () {}, + child: const Icon(Icons.access_alarm), + ), + )); + + await tester.tap(find.byType(RawMaterialButton)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 1); + expect(feedback.hapticCount, 0); + }); + + testWidgets('FloatingActionButton with disabled feedback using FloatingActionButtonTheme', (WidgetTester tester) async { + const bool enableFeedbackTheme = false; + final ThemeData theme = ThemeData( + floatingActionButtonTheme: const FloatingActionButtonThemeData( + enableFeedback: enableFeedbackTheme, + ), + ); + + await tester.pumpWidget(MaterialApp( + home: Theme( + data: theme, + child: FloatingActionButton( + onPressed: () {}, + child: const Icon(Icons.access_alarm), + ), + ), + )); + + await tester.tap(find.byType(RawMaterialButton)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 0); + expect(feedback.hapticCount, 0); + }); + + testWidgets('FloatingActionButton.enableFeedback is overriden by FloatingActionButtonThemeData.enableFeedback', (WidgetTester tester) async { + const bool enableFeedbackTheme = false; + const bool enableFeedback = true; + final ThemeData theme = ThemeData( + floatingActionButtonTheme: const FloatingActionButtonThemeData( + enableFeedback: enableFeedbackTheme, + ), + ); + + await tester.pumpWidget(MaterialApp( + home: Theme( + data: theme, + child: FloatingActionButton( + enableFeedback: enableFeedback, + onPressed: () {}, + child: const Icon(Icons.access_alarm), + ), + ), + )); + + await tester.tap(find.byType(RawMaterialButton)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 1); + expect(feedback.hapticCount, 0); + }); + }); } Offset _rightEdgeOfFab(WidgetTester tester) {