Unverified Commit 96a78c08 authored by Aneesh Rao's avatar Aneesh Rao Committed by GitHub

Enable customization of TabBar's InkWell (#69457)

parent 53410c4b
......@@ -16,6 +16,7 @@ import 'debug.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'material_state.dart';
import 'tab_bar_theme.dart';
import 'tab_controller.dart';
import 'tab_indicator.dart';
......@@ -608,7 +609,9 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.overlayColor,
this.mouseCursor,
this.enableFeedback,
this.onTap,
this.physics,
}) : assert(tabs != null),
......@@ -741,6 +744,22 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// bodyText1 definition is used.
final TextStyle? unselectedLabelStyle;
/// Defines the ink response focus, hover, and splash colors.
///
/// If non-null, it is resolved against one of [MaterialState.focused],
/// [MaterialState.hovered], and [MaterialState.pressed].
///
/// [MaterialState.pressed] triggers a ripple (an ink splash), per
/// the current Material Design spec. The [overlayColor] doesn't map
/// a state to [InkResponse.highlightColor] because a separate highlight
/// is not used by the current design guidelines. See
/// https://material.io/design/interaction/states.html#pressed
///
/// If the overlay color is null or resolves to null, then the default values
/// for [InkResponse.focusColor], [InkResponse.hoverColor], [InkResponse.splashColor]
/// will be used instead.
final MaterialStateProperty<Color?>? overlayColor;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
......@@ -750,6 +769,14 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// If this property is null, [SystemMouseCursors.click] will be used.
final MouseCursor? mouseCursor;
/// 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.
///
/// Defaults to true.
final bool? enableFeedback;
/// An optional callback that's called when the [TabBar] is tapped.
///
/// The callback is applied to the index of the tab where the tap occurred.
......@@ -1096,6 +1123,8 @@ class _TabBarState extends State<TabBar> {
wrappedTabs[index] = InkWell(
mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click,
onTap: () { _handleTap(index); },
enableFeedback: widget.enableFeedback ?? true,
overlayColor: widget.overlayColor,
child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
child: Stack(
......
......@@ -13,6 +13,7 @@ import '../flutter_test_alternative.dart' show Fake;
import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
Widget boilerplate({ Widget? child, TextDirection textDirection = TextDirection.ltr }) {
return Localizations(
......@@ -2226,6 +2227,132 @@ void main() {
));
});
group('Tab feedback', () {
late FeedbackTester feedback;
setUp(() {
feedback = FeedbackTester();
});
tearDown(() {
feedback.dispose();
});
testWidgets('Tab feedback is enabled (default)', (WidgetTester tester) async {
await tester.pumpWidget(
boilerplate(
child: const DefaultTabController(
length: 1,
child: TabBar(
tabs: <Tab>[
Tab(text: 'A',)
],
)
)
)
);
await tester.tap(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0);
await tester.tap(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 2);
expect(feedback.hapticCount, 0);
});
testWidgets('Tab feedback is disabled', (WidgetTester tester) async {
await tester.pumpWidget(
boilerplate(
child: const DefaultTabController(
length: 1,
child: TabBar(
tabs: <Tab>[
Tab(text: 'A',)
],
enableFeedback: false,
),
),
),
);
await tester.tap(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
await tester.longPress(find.byType(InkWell), pointer: 1);
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
});
});
group('Tab overlayColor affects ink response', () {
testWidgets('Tab\'s ink well changes color on hover with Tab overlayColor',
(WidgetTester tester) async {
await tester.pumpWidget(
boilerplate(
child: DefaultTabController(
length: 1,
child: TabBar(
tabs: const <Tab>[
Tab(text: 'A',)
],
overlayColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return const Color(0xff00ff00);
if (states.contains(MaterialState.pressed))
return const Color(0xf00fffff);
return const Color(0xffbadbad); // Shouldn't happen.
}
),
)
)
)
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byType(Tab)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(0.0, 276.0, 800.0, 324.0), color: const Color(0xff00ff00)));
});
testWidgets('Tab\'s ink response splashColor matches resolved Tab overlayColor for MaterialState.pressed',
(WidgetTester tester) async {
const Color splashColor = Color(0xf00fffff);
await tester.pumpWidget(
boilerplate(
child: DefaultTabController(
length: 1,
child: TabBar(
tabs: const <Tab>[
Tab(text: 'A',)
],
overlayColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return const Color(0xff00ff00);
if (states.contains(MaterialState.pressed))
return splashColor;
return const Color(0xffbadbad); // Shouldn't happen.
}
),
)
)
)
);
await tester.pumpAndSettle();
final TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center);
await tester.pump(const Duration(milliseconds: 200)); // unconfirmed splash is well underway
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..circle(x: 400, y: 24, color: splashColor));
await gesture.up();
});
});
testWidgets('Skipping tabs with global key does not crash', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/24660
final List<String> tabs = <String>[
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment