diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index c9fd36de39..9a2310b1be 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -52,6 +52,7 @@ class ListTileTheme extends InheritedTheme { this.contentPadding, this.tileColor, this.selectedTileColor, + this.enableFeedback, required Widget child, }) : super(key: key, child: child); @@ -70,6 +71,7 @@ class ListTileTheme extends InheritedTheme { EdgeInsetsGeometry? contentPadding, Color? tileColor, Color? selectedTileColor, + bool? enableFeedback, required Widget child, }) { assert(child != null); @@ -87,6 +89,7 @@ class ListTileTheme extends InheritedTheme { contentPadding: contentPadding ?? parent.contentPadding, tileColor: tileColor ?? parent.tileColor, selectedTileColor: selectedTileColor ?? parent.selectedTileColor, + enableFeedback: enableFeedback ?? parent.enableFeedback, child: child, ); }, @@ -131,6 +134,11 @@ class ListTileTheme extends InheritedTheme { /// If [ListTile.selectedTileColor] is provided, [selectedTileColor] is ignored. final Color? selectedTileColor; + /// If specified, defines the feedback property for `ListTile`. + /// + /// If [ListTile.enableFeedback] is provided, [enableFeedback] is ignored. + final bool? enableFeedback; + /// The closest instance of this class that encloses the given context. /// /// Typical usage is as follows: @@ -155,6 +163,7 @@ class ListTileTheme extends InheritedTheme { contentPadding: contentPadding, tileColor: tileColor, selectedTileColor: selectedTileColor, + enableFeedback: enableFeedback, child: child, ); } @@ -169,7 +178,8 @@ class ListTileTheme extends InheritedTheme { || textColor != oldWidget.textColor || contentPadding != oldWidget.contentPadding || tileColor != oldWidget.tileColor - || selectedTileColor != oldWidget.selectedTileColor; + || selectedTileColor != oldWidget.selectedTileColor + || enableFeedback != oldWidget.enableFeedback; } } @@ -708,6 +718,7 @@ class ListTile extends StatelessWidget { this.autofocus = false, this.tileColor, this.selectedTileColor, + this.enableFeedback, }) : assert(isThreeLine != null), assert(enabled != null), assert(selected != null), @@ -901,6 +912,16 @@ class ListTile extends StatelessWidget { /// if it's not null and to [Colors.transparent] if it's null. final Color? selectedTileColor; + /// 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. + /// + /// See also: + /// + /// * [Feedback] for providing platform-specific feedback to certain actions. + final bool? enableFeedback; + /// Add a one pixel border in between each tile. If color isn't specified the /// [ThemeData.dividerColor] of the context's [Theme] is used. /// @@ -1068,6 +1089,7 @@ class ListTile extends StatelessWidget { const EdgeInsets _defaultContentPadding = EdgeInsets.symmetric(horizontal: 16.0); final TextDirection textDirection = Directionality.of(context); + final bool resolvedEnableFeedback = enableFeedback ?? tileTheme.enableFeedback ?? true; final EdgeInsets resolvedContentPadding = contentPadding?.resolve(textDirection) ?? tileTheme.contentPadding?.resolve(textDirection) ?? _defaultContentPadding; @@ -1090,6 +1112,7 @@ class ListTile extends StatelessWidget { focusColor: focusColor, hoverColor: hoverColor, autofocus: autofocus, + enableFeedback: resolvedEnableFeedback, child: Semantics( selected: selected, enabled: enabled, diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index c03b297e9f..130e7e53dc 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; +import 'feedback_tester.dart'; class TestIcon extends StatefulWidget { const TestIcon({ Key? key }) : super(key: key); @@ -1712,4 +1713,125 @@ void main() { expect(renderBox.size.width, equals(0.0)); expect(renderBox.size.height, equals(0.0)); }); + + group('feedback', () { + late FeedbackTester feedback; + + setUp(() { + feedback = FeedbackTester(); + }); + + tearDown(() { + feedback.dispose(); + }); + + testWidgets('ListTile with disabled feedback', (WidgetTester tester) async { + const bool enableFeedback = false; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ListTile( + title: const Text('Title'), + onTap: () {}, + enableFeedback: enableFeedback, + ), + ), + ), + ); + + await tester.tap(find.byType(ListTile)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 0); + expect(feedback.hapticCount, 0); + }); + + testWidgets('ListTile with enabled feedback', (WidgetTester tester) async { + const bool enableFeedback = true; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ListTile( + title: const Text('Title'), + onTap: () {}, + enableFeedback: enableFeedback, + ), + ), + ), + ); + + await tester.tap(find.byType(ListTile)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 1); + expect(feedback.hapticCount, 0); + }); + + testWidgets('ListTile with enabled feedback by default', (WidgetTester tester) async { + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ListTile( + title: const Text('Title'), + onTap: () {}, + ), + ), + ), + ); + + await tester.tap(find.byType(ListTile)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 1); + expect(feedback.hapticCount, 0); + }); + + testWidgets('ListTile with disabled feedback using ListTileTheme', (WidgetTester tester) async { + const bool enableFeedbackTheme = false; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ListTileTheme( + enableFeedback: enableFeedbackTheme, + child: ListTile( + title: const Text('Title'), + onTap: () {}, + ), + ), + ), + ), + ); + + await tester.tap(find.byType(ListTile)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 0); + expect(feedback.hapticCount, 0); + }); + + testWidgets('ListTile.enableFeedback overrides ListTileTheme.enableFeedback', (WidgetTester tester) async { + const bool enableFeedbackTheme = false; + const bool enableFeedback = true; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ListTileTheme( + enableFeedback: enableFeedbackTheme, + child: ListTile( + enableFeedback: enableFeedback, + title: const Text('Title'), + onTap: () {}, + ), + ), + ), + ), + ); + + await tester.tap(find.byType(ListTile)); + await tester.pump(const Duration(seconds: 1)); + expect(feedback.clickSoundCount, 1); + expect(feedback.hapticCount, 0); + }); + }); }