From 4c1d015e9e80dbc1d3b09b239fb6d2a86ff3a9fb Mon Sep 17 00:00:00 2001 From: Darren Austin Date: Thu, 5 Nov 2020 23:54:01 -0800 Subject: [PATCH] Reland "Updated SearchDelegate to follow custom InputDecorationTheme" (#69653) --- packages/flutter/lib/src/material/search.dart | 107 +++++++++++------- .../flutter/test/material/search_test.dart | 73 ++++++++---- 2 files changed, 116 insertions(+), 64 deletions(-) diff --git a/packages/flutter/lib/src/material/search.dart b/packages/flutter/lib/src/material/search.dart index c4348c1646..c5bb50f042 100644 --- a/packages/flutter/lib/src/material/search.dart +++ b/packages/flutter/lib/src/material/search.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'app_bar.dart'; +import 'app_bar_theme.dart'; +import 'color_scheme.dart'; import 'colors.dart'; import 'debug.dart'; import 'input_border.dart'; @@ -92,9 +94,10 @@ Future showSearch({ /// ## Handling emojis and other complex characters /// {@macro flutter.widgets.EditableText.onChanged} abstract class SearchDelegate { - - /// Constructor to be called by subclasses which may specify [searchFieldLabel], [keyboardType] and/or - /// [textInputAction]. + /// Constructor to be called by subclasses which may specify + /// [searchFieldLabel], either [searchFieldStyle] or [searchFieldDecorationTheme], + /// [keyboardType] and/or [textInputAction]. Only one of [searchFieldLabel] + /// and [searchFieldDecorationTheme] may be non-null. /// /// {@tool snippet} /// ```dart @@ -124,9 +127,10 @@ abstract class SearchDelegate { SearchDelegate({ this.searchFieldLabel, this.searchFieldStyle, + this.searchFieldDecorationTheme, this.keyboardType, this.textInputAction = TextInputAction.search, - }); + }) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null); /// Suggestions shown in the body of the search page while the user types a /// query into the search field. @@ -184,27 +188,39 @@ abstract class SearchDelegate { /// * [AppBar.actions], the intended use for the return value of this method. List buildActions(BuildContext context); - /// The theme used to style the [AppBar]. + /// The theme used to configure the search page. /// - /// By default, a white theme is used. + /// The returned [ThemeData] will be used to wrap the entire search page, + /// so it can be used to configure any of its components with the appropriate + /// theme properties. + /// + /// Unless overridden, the default theme will configure the AppBar containing + /// the search input text field with a white background and black text on light + /// themes. For dark themes the default is a dark grey background with light + /// color text. /// /// See also: /// - /// * [AppBar.backgroundColor], which is set to [ThemeData.primaryColor]. - /// * [AppBar.iconTheme], which is set to [ThemeData.primaryIconTheme]. - /// * [AppBar.textTheme], which is set to [ThemeData.primaryTextTheme]. - /// * [AppBar.brightness], which is set to [ThemeData.primaryColorBrightness]. + /// * [AppBarTheme], which configures the AppBar's appearance. + /// * [InputDecorationTheme], which configures the appearance of the search + /// text field. ThemeData appBarTheme(BuildContext context) { assert(context != null); final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; assert(theme != null); return theme.copyWith( - primaryColor: theme.brightness == Brightness.dark - ? theme.primaryColor - : Colors.white, - primaryIconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), - primaryColorBrightness: Brightness.light, - primaryTextTheme: theme.textTheme, + appBarTheme: AppBarTheme( + brightness: colorScheme.brightness, + color: colorScheme.brightness == Brightness.dark ? Colors.grey[900] : Colors.white, + iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), + textTheme: theme.textTheme, + ), + inputDecorationTheme: searchFieldDecorationTheme ?? + InputDecorationTheme( + hintStyle: searchFieldStyle ?? theme.inputDecorationTheme.hintStyle, + border: InputBorder.none, + ), ); } @@ -276,8 +292,17 @@ abstract class SearchDelegate { /// /// If this value is set to null, the value of the ambient [Theme]'s /// [InputDecorationTheme.hintStyle] will be used instead. + /// + /// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can + /// be non-null. final TextStyle? searchFieldStyle; + /// The [InputDecorationTheme] used to configure the search field's visuals. + /// + /// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can + /// be non-null. + final InputDecorationTheme? searchFieldDecorationTheme; + /// The type of action button to use for the keyboard. /// /// Defaults to the default value specified in [TextField]. @@ -482,8 +507,6 @@ class _SearchPageState extends State<_SearchPage> { final ThemeData theme = widget.delegate.appBarTheme(context); final String searchFieldLabel = widget.delegate.searchFieldLabel ?? MaterialLocalizations.of(context).searchFieldLabel; - final TextStyle? searchFieldStyle = widget.delegate.searchFieldStyle - ?? theme.inputDecorationTheme.hintStyle; Widget? body; switch(widget.delegate._currentBody) { case _SearchBody.suggestions: @@ -501,7 +524,8 @@ class _SearchPageState extends State<_SearchPage> { case null: break; } - final String routeName; + + late final String routeName; switch (theme.platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: @@ -519,33 +543,28 @@ class _SearchPageState extends State<_SearchPage> { scopesRoute: true, namesRoute: true, label: routeName, - child: Scaffold( - appBar: AppBar( - backgroundColor: theme.primaryColor, - iconTheme: theme.primaryIconTheme, - textTheme: theme.primaryTextTheme, - brightness: theme.primaryColorBrightness, - leading: widget.delegate.buildLeading(context), - title: TextField( - controller: widget.delegate._queryTextController, - focusNode: focusNode, - style: theme.textTheme.headline6, - textInputAction: widget.delegate.textInputAction, - keyboardType: widget.delegate.keyboardType, - onSubmitted: (String _) { - widget.delegate.showResults(context); - }, - decoration: InputDecoration( - border: InputBorder.none, - hintText: searchFieldLabel, - hintStyle: searchFieldStyle, + child: Theme( + data: theme, + child: Scaffold( + appBar: AppBar( + leading: widget.delegate.buildLeading(context), + title: TextField( + controller: widget.delegate._queryTextController, + focusNode: focusNode, + style: theme.textTheme.headline6, + textInputAction: widget.delegate.textInputAction, + keyboardType: widget.delegate.keyboardType, + onSubmitted: (String _) { + widget.delegate.showResults(context); + }, + decoration: InputDecoration(hintText: searchFieldLabel), ), + actions: widget.delegate.buildActions(context), + ), + body: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: body, ), - actions: widget.delegate.buildActions(context), - ), - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: body, ), ), ); diff --git a/packages/flutter/test/material/search_test.dart b/packages/flutter/test/material/search_test.dart index 94328acafe..5a564c589b 100644 --- a/packages/flutter/test/material/search_test.dart +++ b/packages/flutter/test/material/search_test.dart @@ -122,7 +122,8 @@ void main() { }); testWidgets('Hint text color overridden', (WidgetTester tester) async { - final _TestSearchDelegate delegate = _TestSearchDelegate(); + const String searchHintText = 'Enter search terms'; + final _TestSearchDelegate delegate = _TestSearchDelegate(searchHint: searchHintText); await tester.pumpWidget(TestHomePage( delegate: delegate, @@ -130,9 +131,8 @@ void main() { await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); - final TextField textField = tester.widget(find.byType(TextField)); - final Color hintColor = textField.decoration!.hintStyle!.color!; - expect(hintColor, delegate.hintTextColor); + final Text hintText = tester.widget(find.text(searchHintText)); + expect(hintText.style!.color, _TestSearchDelegate.hintTextColor); }); testWidgets('Requests suggestions', (WidgetTester tester) async { @@ -540,20 +540,18 @@ void main() { }); testWidgets('Custom searchFieldStyle value', (WidgetTester tester) async { - const TextStyle searchStyle = TextStyle(color: Colors.red, fontSize: 3); + const String searchHintText = 'Enter search terms'; + const TextStyle searchFieldStyle = TextStyle(color: Colors.red, fontSize: 3); - final _TestSearchDelegate delegate = _TestSearchDelegate(searchFieldStyle: searchStyle); + final _TestSearchDelegate delegate = _TestSearchDelegate(searchHint: searchHintText, searchFieldStyle: searchFieldStyle); - await tester.pumpWidget( - TestHomePage( - delegate: delegate, - )); + await tester.pumpWidget(TestHomePage(delegate: delegate)); await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); - final TextField textField = tester.widget(find.byType(TextField)); - final TextStyle hintStyle = textField.decoration!.hintStyle!; - expect(hintStyle, delegate.searchFieldStyle); + final Text hintText = tester.widget(find.text(searchHintText)); + expect(hintText.style?.color, delegate.searchFieldStyle?.color); + expect(hintText.style?.fontSize, delegate.searchFieldStyle?.fontSize); }); testWidgets('keyboard show search button by default', (WidgetTester tester) async { @@ -692,6 +690,23 @@ void main() { }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); }); + testWidgets('Custom searchFieldDecorationTheme value', + (WidgetTester tester) async { + const InputDecorationTheme searchFieldDecorationTheme = InputDecorationTheme( + hintStyle: TextStyle(color: _TestSearchDelegate.hintTextColor), + ); + final _TestSearchDelegate delegate = _TestSearchDelegate( + searchFieldDecorationTheme: searchFieldDecorationTheme, + ); + + await tester.pumpWidget(TestHomePage(delegate: delegate)); + await tester.tap(find.byTooltip('Search')); + await tester.pumpAndSettle(); + + final ThemeData textFieldTheme = Theme.of(tester.element(find.byType(TextField))); + expect(textFieldTheme.inputDecorationTheme, searchFieldDecorationTheme); + }); + // Regression test for: https://github.com/flutter/flutter/issues/66781 testWidgets('text in search bar contrasts background (light mode)', (WidgetTester tester) async { final ThemeData themeData = ThemeData.light(); @@ -709,8 +724,11 @@ void main() { await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); - final AppBar appBar = tester.widget(find.byType(AppBar)); - expect(appBar.backgroundColor, Colors.white); + final Material appBarBackground = tester.widget(find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), + )); + expect(appBarBackground.color, Colors.white); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.style!.color, themeData.textTheme.bodyText1!.color); @@ -734,8 +752,11 @@ void main() { await tester.tap(find.byTooltip('Search')); await tester.pumpAndSettle(); - final AppBar appBar = tester.widget(find.byType(AppBar)); - expect(appBar.backgroundColor, themeData.primaryColor); + final Material appBarBackground = tester.widget(find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), + )); + expect(appBarBackground.color, themeData.primaryColor); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.style!.color, themeData.textTheme.bodyText1!.color); @@ -803,16 +824,22 @@ class _TestSearchDelegate extends SearchDelegate { this.result = 'Result', this.actions = const [], this.defaultAppBarTheme = false, + InputDecorationTheme? searchFieldDecorationTheme, TextStyle? searchFieldStyle, String? searchHint, TextInputAction textInputAction = TextInputAction.search, - }) : super(searchFieldLabel: searchHint, textInputAction: textInputAction, searchFieldStyle: searchFieldStyle); + }) : super( + searchFieldLabel: searchHint, + textInputAction: textInputAction, + searchFieldStyle: searchFieldStyle, + searchFieldDecorationTheme: searchFieldDecorationTheme, + ); final bool defaultAppBarTheme; final String suggestions; final String result; final List actions; - final Color hintTextColor = Colors.green; + static const Color hintTextColor = Colors.green; @override ThemeData appBarTheme(BuildContext context) { @@ -821,7 +848,13 @@ class _TestSearchDelegate extends SearchDelegate { } final ThemeData theme = Theme.of(context); return theme.copyWith( - inputDecorationTheme: InputDecorationTheme(hintStyle: TextStyle(color: hintTextColor)), + inputDecorationTheme: searchFieldDecorationTheme ?? + InputDecorationTheme( + hintStyle: searchFieldStyle ?? + const TextStyle( + color: hintTextColor, + ), + ), ); }