Create SearchAnchor and SearchViewTheme Widget (#123256)

This commit is contained in:
Qun Cheng 2023-03-27 23:31:11 -07:00 committed by GitHub
parent 9a7387c72e
commit 0300cfa603
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2535 additions and 51 deletions

View File

@ -46,6 +46,7 @@ import 'package:gen_defaults/popup_menu_template.dart';
import 'package:gen_defaults/progress_indicator_template.dart';
import 'package:gen_defaults/radio_template.dart';
import 'package:gen_defaults/search_bar_template.dart';
import 'package:gen_defaults/search_view_template.dart';
import 'package:gen_defaults/segmented_button_template.dart';
import 'package:gen_defaults/slider_template.dart';
import 'package:gen_defaults/snackbar_template.dart';
@ -177,6 +178,7 @@ Future<void> main(List<String> args) async {
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile();
SearchBarTemplate('SearchBar', '$materialLib/search_anchor.dart', tokens).updateFile();
SearchViewTemplate('SearchView', '$materialLib/search_anchor.dart', tokens).updateFile();
SegmentedButtonTemplate('md.comp.outlined-segmented-button', 'SegmentedButton', '$materialLib/segmented_button.dart', tokens).updateFile();
SnackbarTemplate('md.comp.snackbar', 'Snackbar', '$materialLib/snack_bar.dart', tokens).updateFile();
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();

View File

@ -0,0 +1,54 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'template.dart';
class SearchViewTemplate extends TokenTemplate {
const SearchViewTemplate(super.blockName, super.fileName, super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.'
});
@override
String generate() => '''
class _${blockName}DefaultsM3 extends ${blockName}ThemeData {
_${blockName}DefaultsM3(this.context, {required this.isFullScreen});
final BuildContext context;
final bool isFullScreen;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
static double fullScreenBarHeight = ${tokens['md.comp.search-view.full-screen.header.container.height']};
@override
Color? get backgroundColor => ${componentColor('md.comp.search-view.container')};
@override
double? get elevation => ${elevation('md.comp.search-view.container')};
@override
Color? get surfaceTintColor => ${colorOrTransparent('md.comp.search-view.container.surface-tint-layer.color')};
// No default side
@override
OutlinedBorder? get shape => isFullScreen
? ${shape('md.comp.search-view.full-screen.container')}
: ${shape('md.comp.search-view.docked.container')};
@override
TextStyle? get headerTextStyle => ${textStyleWithColor('md.comp.search-view.header.input-text')};
@override
TextStyle? get headerHintStyle => ${textStyleWithColor('md.comp.search-view.header.supporting-text')};
@override
BoxConstraints get constraints => const BoxConstraints(minWidth: 360.0, minHeight: 240.0);
@override
Color? get dividerColor => ${componentColor('md.comp.search-view.divider')};
}
''';
}

View File

@ -0,0 +1,124 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for [SearchAnchor.bar].
import 'package:flutter/material.dart';
void main() => runApp(const SearchBarApp());
class SearchBarApp extends StatefulWidget {
const SearchBarApp({super.key});
@override
State<SearchBarApp> createState() => _SearchBarAppState();
}
class _SearchBarAppState extends State<SearchBarApp> {
Color? selectedColorSeed;
List<ColorLabel> searchHistory = <ColorLabel>[];
Iterable<Widget> getHistoryList(SearchController controller) {
return searchHistory.map((ColorLabel color) => ListTile(
leading: const Icon(Icons.history),
title: Text(color.label),
trailing: IconButton(icon: const Icon(Icons.call_missed), onPressed: () {
controller.text = color.label;
controller.selection = TextSelection.collapsed(offset: controller.text.length);
}),
));
}
Iterable<Widget> getSuggestions(SearchController controller) {
final String input = controller.value.text;
return ColorLabel.values.where((ColorLabel color) => color.label.contains(input))
.map((ColorLabel filteredColor) =>
ListTile(
leading: CircleAvatar(backgroundColor: filteredColor.color),
title: Text(filteredColor.label),
trailing: IconButton(icon: const Icon(Icons.call_missed), onPressed: () {
controller.text = filteredColor.label;
controller.selection = TextSelection.collapsed(offset: controller.text.length);
}),
onTap: () {
controller.closeView(filteredColor.label);
handleSelection(filteredColor);
},
));
}
void handleSelection(ColorLabel selectedColor) {
setState(() {
selectedColorSeed = selectedColor.color;
if (searchHistory.length >= 5) {
searchHistory.removeLast();
}
searchHistory.insert(0, selectedColor);
});
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: selectedColorSeed);
final ColorScheme colors = themeData.colorScheme;
return MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(title: const Text('Search Bar Sample')),
body: Align(
alignment: Alignment.topCenter,
child: Column(
children: <Widget>[
SearchAnchor.bar(
barHintText: 'Search colors',
suggestionsBuilder: (BuildContext context, SearchController controller) {
if (controller.text.isEmpty) {
if (searchHistory.isNotEmpty) {
return getHistoryList(controller);
}
return <Widget>[ Center(child: Text('No search history.', style: TextStyle(color: colors.outline))) ];
}
return getSuggestions(controller);
},
),
cardSize,
Card(color: colors.primary, child: cardSize),
Card(color: colors.onPrimary, child: cardSize),
Card(color: colors.primaryContainer, child: cardSize),
Card(color: colors.onPrimaryContainer, child: cardSize),
Card(color: colors.secondary, child: cardSize),
Card(color: colors.onSecondary, child: cardSize),
],
),
),
),
);
}
}
SizedBox cardSize = const SizedBox(width: 80, height: 30,);
enum ColorLabel {
red('red', Colors.red),
orange('orange', Colors.orange),
yellow('yellow', Colors.yellow),
green('green', Colors.green),
blue('blue', Colors.blue),
indigo('indigo', Colors.indigo),
violet('violet', Color(0xFF8F00FF)),
purple('purple', Colors.purple),
pink('pink', Colors.pink),
silver('silver', Color(0xFF808080)),
gold('gold', Color(0xFFFFD700)),
beige('beige', Color(0xFFF5F5DC)),
brown('brown', Colors.brown),
grey('grey', Colors.grey),
black('black', Colors.black),
white('white', Colors.white);
const ColorLabel(this.label, this.color);
final String label;
final Color color;
}

View File

@ -0,0 +1,87 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for pinned [SearchAnchor] while scrolling.
import 'package:flutter/material.dart';
void main() {
runApp(const PinnedSearchBarApp());
}
class PinnedSearchBarApp extends StatefulWidget {
const PinnedSearchBarApp({super.key});
@override
State<PinnedSearchBarApp> createState() => _PinnedSearchBarAppState();
}
class _PinnedSearchBarAppState extends State<PinnedSearchBarApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xff6750a4)
),
home: Scaffold(
body: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
clipBehavior: Clip.none,
shape: const StadiumBorder(),
scrolledUnderElevation: 0.0,
titleSpacing: 0.0,
backgroundColor: Colors.transparent,
floating: true, // We can also uncomment this line and set `pinned` to true to see a pinned search bar.
title: SearchAnchor.bar(
suggestionsBuilder: (BuildContext context, SearchController controller) {
return List<Widget>.generate(5, (int index) {
return ListTile(
titleAlignment: ListTileTitleAlignment.center,
title: Text('Initial list item $index'),
);
});
}
),
),
// The listed items below are just for filling the screen
// so we can see the scrolling effect.
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
height: 100.0,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return SizedBox(
width: 100.0,
child: Card(
child: Center(child: Text('Card $index')),
),
);
},
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
height: 1000,
color: Colors.deepPurple.withOpacity(0.5),
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,65 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for [SearchAnchor].
import 'package:flutter/material.dart';
void main() => runApp(const SearchBarApp());
class SearchBarApp extends StatefulWidget {
const SearchBarApp({super.key});
@override
State<SearchBarApp> createState() => _SearchBarAppState();
}
class _SearchBarAppState extends State<SearchBarApp> {
final SearchController controller = SearchController();
@override
Widget build(BuildContext context) {
final ThemeData themeData = ThemeData(useMaterial3: true);
return MaterialApp(
theme: themeData,
home: Scaffold(
appBar: AppBar(title: const Text('Search Anchor Sample')),
body: Column(
children: <Widget>[
SearchAnchor(
searchController: controller,
builder: (BuildContext context, SearchController controller) {
return IconButton(
icon: const Icon(Icons.search),
onPressed: () {
controller.openView();
},
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return List<ListTile>.generate(5, (int index) {
final String item = 'item $index';
return ListTile(
title: Text(item),
onTap: () {
setState(() {
controller.closeView(item);
});
},
);
});
}
),
Center(
child: controller.text.isEmpty
? const Text('No item selected')
: Text('Selected item: ${controller.value.text}'),
),
],
),
),
);
}
}

View File

@ -154,6 +154,7 @@ export 'src/material/scrollbar_theme.dart';
export 'src/material/search.dart';
export 'src/material/search_anchor.dart';
export 'src/material/search_bar_theme.dart';
export 'src/material/search_view_theme.dart';
export 'src/material/segmented_button.dart';
export 'src/material/segmented_button_theme.dart';
export 'src/material/selectable_text.dart';

View File

@ -204,6 +204,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.clipBehavior,
}) : assert(elevation == null || elevation >= 0.0),
preferredSize = _PreferredAppBarSize(toolbarHeight, bottom?.preferredSize.height);
@ -714,6 +715,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// {@endtemplate}
final bool forceMaterialTransparency;
/// {@macro flutter.material.Material.clipBehavior}
final Clip? clipBehavior;
bool _getEffectiveCenterTitle(ThemeData theme) {
bool platformCenter() {
switch (theme.platform) {
@ -1044,6 +1048,7 @@ class _AppBarState extends State<AppBar> {
// If the toolbar is allocated less than toolbarHeight make it
// appear to scroll upwards within its shrinking container.
Widget appBar = ClipRect(
clipBehavior: widget.clipBehavior ?? Clip.hardEdge,
child: CustomSingleChildLayout(
delegate: _ToolbarContainerLayout(toolbarHeight),
child: IconTheme.merge(
@ -1186,6 +1191,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
required this.titleTextStyle,
required this.systemOverlayStyle,
required this.forceMaterialTransparency,
required this.clipBehavior
}) : assert(primary || topPadding == 0.0),
_bottomHeight = bottom?.preferredSize.height ?? 0.0;
@ -1221,6 +1227,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final SystemUiOverlayStyle? systemOverlayStyle;
final double _bottomHeight;
final bool forceMaterialTransparency;
final Clip? clipBehavior;
@override
double get minExtent => collapsedHeight;
@ -1259,6 +1266,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
toolbarOpacity: toolbarOpacity,
isScrolledUnder: isScrolledUnder,
child: AppBar(
clipBehavior: clipBehavior,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
title: title,
@ -1463,6 +1471,7 @@ class SliverAppBar extends StatefulWidget {
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.clipBehavior,
}) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
assert(stretchTriggerOffset > 0.0),
assert(collapsedHeight == null || collapsedHeight >= toolbarHeight, 'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].');
@ -1929,6 +1938,9 @@ class SliverAppBar extends StatefulWidget {
/// This property is used to configure an [AppBar].
final bool forceMaterialTransparency;
/// {@macro flutter.material.Material.clipBehavior}
final Clip? clipBehavior;
@override
State<SliverAppBar> createState() => _SliverAppBarState();
}
@ -2035,6 +2047,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
titleTextStyle: widget.titleTextStyle,
systemOverlayStyle: widget.systemOverlayStyle,
forceMaterialTransparency: widget.forceMaterialTransparency,
clipBehavior: widget.clipBehavior,
),
),
);

View File

@ -1322,19 +1322,19 @@ class _BottomSheetDefaultsM3 extends BottomSheetThemeData {
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
Color get backgroundColor => _colors.surface;
Color? get backgroundColor => _colors.surface;
@override
Color get surfaceTintColor => _colors.surfaceTint;
Color? get surfaceTintColor => _colors.surfaceTint;
@override
Color get shadowColor => Colors.transparent;
Color? get shadowColor => Colors.transparent;
@override
Color get dragHandleColor => Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4);
Color? get dragHandleColor => _colors.onSurfaceVariant.withOpacity(0.4);
@override
Size get dragHandleSize => const Size(32, 4);
Size? get dragHandleSize => const Size(32, 4);
}
// END GENERATED TOKEN PROPERTIES - BottomSheet

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,217 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'theme.dart';
// Examples can assume:
// late BuildContext context;
/// Defines the configuration of the search views created by the [SearchAnchor]
/// widget.
///
/// Descendant widgets obtain the current [SearchViewThemeData] object using
/// `SearchViewTheme.of(context)`.
///
/// Typically, a [SearchViewThemeData] is specified as part of the overall [Theme]
/// with [ThemeData.searchViewTheme]. Otherwise, [SearchViewTheme] can be used
/// to configure its own widget subtree.
///
/// All [SearchViewThemeData] properties are `null` by default. If any of these
/// properties are null, the search view will provide its own defaults.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme for the application.
/// * [SearchBarThemeData], which describes the theme for the search bar itself in a
/// [SearchBar] widget.
/// * [SearchAnchor], which is used to open a search view route.
@immutable
class SearchViewThemeData with Diagnosticable {
/// Creates a theme that can be used for [ThemeData.searchViewTheme].
const SearchViewThemeData({
this.backgroundColor,
this.elevation,
this.surfaceTintColor,
this.constraints,
this.side,
this.shape,
this.headerTextStyle,
this.headerHintStyle,
this.dividerColor,
});
/// Overrides the default value of the [SearchAnchor.viewBackgroundColor].
final Color? backgroundColor;
/// Overrides the default value of the [SearchAnchor.viewElevation].
final double? elevation;
/// Overrides the default value of the [SearchAnchor.viewSurfaceTintColor].
final Color? surfaceTintColor;
/// Overrides the default value of the [SearchAnchor.viewSide].
final BorderSide? side;
/// Overrides the default value of the [SearchAnchor.viewShape].
final OutlinedBorder? shape;
/// Overrides the default value for [SearchAnchor.headerTextStyle].
final TextStyle? headerTextStyle;
/// Overrides the default value for [SearchAnchor.headerHintStyle].
final TextStyle? headerHintStyle;
/// Overrides the value of size constraints for [SearchAnchor.viewConstraints].
final BoxConstraints? constraints;
/// Overrides the value of the divider color for [SearchAnchor.dividerColor].
final Color? dividerColor;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
SearchViewThemeData copyWith({
Color? backgroundColor,
double? elevation,
Color? surfaceTintColor,
BorderSide? side,
OutlinedBorder? shape,
TextStyle? headerTextStyle,
TextStyle? headerHintStyle,
BoxConstraints? constraints,
Color? dividerColor,
}) {
return SearchViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
elevation: elevation ?? this.elevation,
surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
side: side ?? this.side,
shape: shape ?? this.shape,
headerTextStyle: headerTextStyle ?? this.headerTextStyle,
headerHintStyle: headerHintStyle ?? this.headerHintStyle,
constraints: constraints ?? this.constraints,
dividerColor: dividerColor ?? this.dividerColor,
);
}
/// Linearly interpolate between two [SearchViewThemeData]s.
static SearchViewThemeData? lerp(SearchViewThemeData? a, SearchViewThemeData? b, double t) {
if (identical(a, b)) {
return a;
}
return SearchViewThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t),
side: _lerpSides(a?.side, b?.side, t),
shape: OutlinedBorder.lerp(a?.shape, b?.shape, t),
headerTextStyle: TextStyle.lerp(a?.headerTextStyle, b?.headerTextStyle, t),
headerHintStyle: TextStyle.lerp(a?.headerTextStyle, b?.headerTextStyle, t),
constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t),
dividerColor: Color.lerp(a?.dividerColor, b?.dividerColor, t),
);
}
@override
int get hashCode => Object.hash(
backgroundColor,
elevation,
surfaceTintColor,
side,
shape,
headerTextStyle,
headerHintStyle,
constraints,
dividerColor,
);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is SearchViewThemeData
&& other.backgroundColor == backgroundColor
&& other.elevation == elevation
&& other.surfaceTintColor == surfaceTintColor
&& other.side == side
&& other.shape == shape
&& other.headerTextStyle == headerTextStyle
&& other.headerHintStyle == headerHintStyle
&& other.constraints == constraints
&& other.dividerColor == dividerColor;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Color?>('backgroundColor', backgroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<double?>('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<Color?>('surfaceTintColor', surfaceTintColor, defaultValue: null));
properties.add(DiagnosticsProperty<BorderSide?>('side', side, defaultValue: null));
properties.add(DiagnosticsProperty<OutlinedBorder?>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle?>('headerTextStyle', headerTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle?>('headerHintStyle', headerHintStyle, defaultValue: null));
properties.add(DiagnosticsProperty<BoxConstraints>('constraints', constraints, defaultValue: null));
properties.add(DiagnosticsProperty<Color?>('dividerColor', dividerColor, defaultValue: null));
}
// Special case because BorderSide.lerp() doesn't support null arguments
static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) {
if (a == null || b == null) {
return null;
}
if (identical(a, b)) {
return a;
}
return BorderSide.lerp(a, b, t);
}
}
/// An inherited widget that defines the configuration in this widget's
/// descendants for search view created by the [SearchAnchor] widget.
///
/// A search view theme can be specified as part of the overall Material theme using
/// [ThemeData.searchViewTheme].
///
/// See also:
///
/// * [SearchViewThemeData], which describes the actual configuration of a search view
/// theme.
class SearchViewTheme extends InheritedWidget {
/// Creates a const theme that controls the configurations for the search view
/// created by the [SearchAnchor] widget.
const SearchViewTheme({
super.key,
required this.data,
required super.child,
});
/// The properties used for all descendant [SearchAnchor] widgets.
final SearchViewThemeData data;
/// Returns the configuration [data] from the closest [SearchViewTheme] ancestor.
/// If there is no ancestor, it returns [ThemeData.searchViewTheme].
///
/// Typical usage is as follows:
///
/// ```dart
/// SearchViewThemeData theme = SearchViewTheme.of(context);
/// ```
static SearchViewThemeData of(BuildContext context) {
final SearchViewTheme? searchViewTheme = context.dependOnInheritedWidgetOfExactType<SearchViewTheme>();
return searchViewTheme?.data ?? Theme.of(context).searchViewTheme;
}
@override
bool updateShouldNotify(SearchViewTheme oldWidget) => data != oldWidget.data;
}

View File

@ -54,6 +54,7 @@ import 'progress_indicator_theme.dart';
import 'radio_theme.dart';
import 'scrollbar_theme.dart';
import 'search_bar_theme.dart';
import 'search_view_theme.dart';
import 'segmented_button_theme.dart';
import 'slider_theme.dart';
import 'snack_bar_theme.dart';
@ -375,6 +376,7 @@ class ThemeData with Diagnosticable {
ProgressIndicatorThemeData? progressIndicatorTheme,
RadioThemeData? radioTheme,
SearchBarThemeData? searchBarTheme,
SearchViewThemeData? searchViewTheme,
SegmentedButtonThemeData? segmentedButtonTheme,
SliderThemeData? sliderTheme,
SnackBarThemeData? snackBarTheme,
@ -590,6 +592,7 @@ class ThemeData with Diagnosticable {
progressIndicatorTheme ??= const ProgressIndicatorThemeData();
radioTheme ??= const RadioThemeData();
searchBarTheme ??= const SearchBarThemeData();
searchViewTheme ??= const SearchViewThemeData();
segmentedButtonTheme ??= const SegmentedButtonThemeData();
sliderTheme ??= const SliderThemeData();
snackBarTheme ??= const SnackBarThemeData();
@ -688,6 +691,7 @@ class ThemeData with Diagnosticable {
progressIndicatorTheme: progressIndicatorTheme,
radioTheme: radioTheme,
searchBarTheme: searchBarTheme,
searchViewTheme: searchViewTheme,
segmentedButtonTheme: segmentedButtonTheme,
sliderTheme: sliderTheme,
snackBarTheme: snackBarTheme,
@ -800,6 +804,7 @@ class ThemeData with Diagnosticable {
required this.progressIndicatorTheme,
required this.radioTheme,
required this.searchBarTheme,
required this.searchViewTheme,
required this.segmentedButtonTheme,
required this.sliderTheme,
required this.snackBarTheme,
@ -1492,6 +1497,9 @@ class ThemeData with Diagnosticable {
/// A theme for customizing the appearance and layout of [SearchBar] widgets.
final SearchBarThemeData searchBarTheme;
/// A theme for customizing the appearance and layout of search views created by [SearchAnchor] widgets.
final SearchViewThemeData searchViewTheme;
/// A theme for customizing the appearance and layout of [SegmentedButton] widgets.
final SegmentedButtonThemeData segmentedButtonTheme;
@ -1704,6 +1712,7 @@ class ThemeData with Diagnosticable {
ProgressIndicatorThemeData? progressIndicatorTheme,
RadioThemeData? radioTheme,
SearchBarThemeData? searchBarTheme,
SearchViewThemeData? searchViewTheme,
SegmentedButtonThemeData? segmentedButtonTheme,
SliderThemeData? sliderTheme,
SnackBarThemeData? snackBarTheme,
@ -1839,6 +1848,7 @@ class ThemeData with Diagnosticable {
progressIndicatorTheme: progressIndicatorTheme ?? this.progressIndicatorTheme,
radioTheme: radioTheme ?? this.radioTheme,
searchBarTheme: searchBarTheme ?? this.searchBarTheme,
searchViewTheme: searchViewTheme ?? this.searchViewTheme,
segmentedButtonTheme: segmentedButtonTheme ?? this.segmentedButtonTheme,
sliderTheme: sliderTheme ?? this.sliderTheme,
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
@ -2034,6 +2044,7 @@ class ThemeData with Diagnosticable {
progressIndicatorTheme: ProgressIndicatorThemeData.lerp(a.progressIndicatorTheme, b.progressIndicatorTheme, t)!,
radioTheme: RadioThemeData.lerp(a.radioTheme, b.radioTheme, t),
searchBarTheme: SearchBarThemeData.lerp(a.searchBarTheme, b.searchBarTheme, t)!,
searchViewTheme: SearchViewThemeData.lerp(a.searchViewTheme, b.searchViewTheme, t)!,
segmentedButtonTheme: SegmentedButtonThemeData.lerp(a.segmentedButtonTheme, b.segmentedButtonTheme, t),
sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
snackBarTheme: SnackBarThemeData.lerp(a.snackBarTheme, b.snackBarTheme, t),
@ -2141,6 +2152,7 @@ class ThemeData with Diagnosticable {
other.progressIndicatorTheme == progressIndicatorTheme &&
other.radioTheme == radioTheme &&
other.searchBarTheme == searchBarTheme &&
other.searchViewTheme == searchViewTheme &&
other.segmentedButtonTheme == segmentedButtonTheme &&
other.sliderTheme == sliderTheme &&
other.snackBarTheme == snackBarTheme &&
@ -2245,6 +2257,7 @@ class ThemeData with Diagnosticable {
progressIndicatorTheme,
radioTheme,
searchBarTheme,
searchViewTheme,
segmentedButtonTheme,
sliderTheme,
snackBarTheme,
@ -2351,6 +2364,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<ProgressIndicatorThemeData>('progressIndicatorTheme', progressIndicatorTheme, defaultValue: defaultData.progressIndicatorTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<RadioThemeData>('radioTheme', radioTheme, defaultValue: defaultData.radioTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SearchBarThemeData>('searchBarTheme', searchBarTheme, defaultValue: defaultData.searchBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SearchViewThemeData>('searchViewTheme', searchViewTheme, defaultValue: defaultData.searchViewTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SegmentedButtonThemeData>('segmentedButtonTheme', segmentedButtonTheme, defaultValue: defaultData.segmentedButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SnackBarThemeData>('snackBarTheme', snackBarTheme, defaultValue: defaultData.snackBarTheme, level: DiagnosticLevel.debug));

View File

@ -723,6 +723,706 @@ void main() {
await tester.pump();
expect(helperText.style?.color, hoveredColor);
});
testWidgets('The search view defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final ColorScheme colorScheme = theme.colorScheme;
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Material(
child: Align(
alignment: Alignment.topLeft,
child: SearchAnchor(
viewHintText: 'hint text',
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
),
);
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Material material = getSearchViewMaterial(tester);
expect(material.elevation, 6.0);
expect(material.color, colorScheme.surface);
expect(material.surfaceTintColor, colorScheme.surfaceTint);
final Finder findDivider = find.byType(Divider);
final Container dividerContainer = tester.widget<Container>(find.descendant(of: findDivider, matching: find.byType(Container)).first);
final BoxDecoration decoration = dividerContainer.decoration! as BoxDecoration;
expect(decoration.border!.bottom.color, colorScheme.outline);
// Default search view has a leading back button on the start of the header.
expect(find.widgetWithIcon(IconButton, Icons.arrow_back), findsOneWidget);
// Default search view has a trailing close button on the end of the header.
// It is used to clear the input in the text field.
expect(find.widgetWithIcon(IconButton, Icons.close), findsOneWidget);
final Text helperText = tester.widget(find.text('hint text'));
expect(helperText.style?.color, colorScheme.onSurfaceVariant);
expect(helperText.style?.fontSize, 16.0);
expect(helperText.style?.fontFamily, 'Roboto');
expect(helperText.style?.fontWeight, FontWeight.w400);
const String input = 'entered text';
await tester.enterText(find.byType(SearchBar), input);
final EditableText inputText = tester.widget(find.text(input));
expect(inputText.style.color, colorScheme.onSurface);
expect(inputText.style.fontSize, 16.0);
expect(inputText.style.fontFamily, 'Roboto');
expect(inputText.style.fontWeight, FontWeight.w400);
});
testWidgets('The search view default size on different platforms', (WidgetTester tester) async {
// The search view should be is full-screen on mobile platforms,
// and have a size of (360, 2/3 screen height) on other platforms
Widget buildSearchAnchor(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: Scaffold(
body: SafeArea(
child: Material(
child: Align(
alignment: Alignment.topLeft,
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
),
);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.android, TargetPlatform.fuchsia ]) {
await tester.pumpWidget(Container());
await tester.pumpWidget(buildSearchAnchor(platform));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(sizedBox.width, 800.0);
expect(sizedBox.height, 600.0);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(Container());
await tester.pumpWidget(buildSearchAnchor(platform));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(sizedBox.width, 360.0);
expect(sizedBox.height, 400.0);
}
});
testWidgets('SearchAnchor respects isFullScreen property', (WidgetTester tester) async {
Widget buildSearchAnchor(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: Scaffold(
body: SafeArea(
child: Material(
child: Align(
alignment: Alignment.topLeft,
child: SearchAnchor(
isFullScreen: true,
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
),
);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(Container());
await tester.pumpWidget(buildSearchAnchor(platform));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(sizedBox.width, 800.0);
expect(sizedBox.height, 600.0);
}
});
testWidgets('SearchAnchor respects controller property', (WidgetTester tester) async {
const String defaultText = 'initial text';
final SearchController controller = SearchController();
controller.text = defaultText;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor(
searchController: controller,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
);
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(controller.value.text, defaultText);
expect(find.text(defaultText), findsOneWidget);
const String updatedText = 'updated text';
await tester.enterText(find.byType(SearchBar), updatedText);
expect(controller.value.text, updatedText);
expect(find.text(defaultText), findsNothing);
expect(find.text(updatedText), findsOneWidget);
});
testWidgets('SearchAnchor respects viewBuilder property', (WidgetTester tester) async {
Widget buildAnchor({ViewBuilder? viewBuilder}) {
return MaterialApp(
home: Material(
child: SearchAnchor(
viewBuilder: viewBuilder,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
);
}
await tester.pumpWidget(buildAnchor());
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
// Default is a ListView.
expect(find.byType(ListView), findsOneWidget);
await tester.pumpWidget(Container());
await tester.pumpWidget(buildAnchor(viewBuilder: (Iterable<Widget> suggestions)
=> GridView.count(crossAxisCount: 5, children: suggestions.toList(),)
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(find.byType(ListView), findsNothing);
expect(find.byType(GridView), findsOneWidget);
});
testWidgets('SearchAnchor respects viewLeading property', (WidgetTester tester) async {
Widget buildAnchor({Widget? viewLeading}) {
return MaterialApp(
home: Material(
child: SearchAnchor(
viewLeading: viewLeading,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
);
}
await tester.pumpWidget(buildAnchor());
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
// Default is a icon button with arrow_back.
expect(find.widgetWithIcon(IconButton, Icons.arrow_back), findsOneWidget);
await tester.pumpWidget(Container());
await tester.pumpWidget(buildAnchor(viewLeading: const Icon(Icons.history)));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(find.byIcon(Icons.arrow_back), findsNothing);
expect(find.byIcon(Icons.history), findsOneWidget);
});
testWidgets('SearchAnchor respects viewTrailing property', (WidgetTester tester) async {
Widget buildAnchor({Iterable<Widget>? viewTrailing}) {
return MaterialApp(
home: Material(
child: SearchAnchor(
viewTrailing: viewTrailing,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
);
}
await tester.pumpWidget(buildAnchor());
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
// Default is a icon button with close icon.
expect(find.widgetWithIcon(IconButton, Icons.close), findsOneWidget);
await tester.pumpWidget(Container());
await tester.pumpWidget(buildAnchor(viewTrailing: <Widget>[const Icon(Icons.history)]));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(find.byIcon(Icons.close), findsNothing);
expect(find.byIcon(Icons.history), findsOneWidget);
});
testWidgets('SearchAnchor respects viewHintText property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
viewHintText: 'hint text',
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(find.text('hint text'), findsOneWidget);
});
testWidgets('SearchAnchor respects viewBackgroundColor property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
viewBackgroundColor: Colors.purple,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(getSearchViewMaterial(tester).color, Colors.purple);
});
testWidgets('SearchAnchor respects viewElevation property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
viewElevation: 3.0,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(getSearchViewMaterial(tester).elevation, 3.0);
});
testWidgets('SearchAnchor respects viewSurfaceTint property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
viewSurfaceTintColor: Colors.purple,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(getSearchViewMaterial(tester).surfaceTintColor, Colors.purple);
});
testWidgets('SearchAnchor respects viewSide property', (WidgetTester tester) async {
const BorderSide side = BorderSide(color: Colors.purple, width: 5.0);
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
isFullScreen: false,
viewSide: side,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(getSearchViewMaterial(tester).shape, RoundedRectangleBorder(side: side, borderRadius: BorderRadius.circular(28.0)));
});
testWidgets('SearchAnchor respects viewShape property', (WidgetTester tester) async {
const BorderSide side = BorderSide(color: Colors.purple, width: 5.0);
const OutlinedBorder shape = StadiumBorder(side: side);
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
isFullScreen: false,
viewShape: shape,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
expect(getSearchViewMaterial(tester).shape, shape);
});
testWidgets('SearchAnchor respects headerTextStyle property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
headerTextStyle: theme.textTheme.bodyLarge?.copyWith(color: Colors.red),
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
await tester.enterText(find.byType(SearchBar), 'input text');
await tester.pumpAndSettle();
final EditableText inputText = tester.widget(find.text('input text'));
expect(inputText.style.color, Colors.red);
});
testWidgets('SearchAnchor respects headerHintStyle property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
viewHintText: 'hint text',
headerHintStyle: theme.textTheme.bodyLarge?.copyWith(color: Colors.orange),
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
final Text inputText = tester.widget(find.text('hint text'));
expect(inputText.style?.color, Colors.orange);
});
testWidgets('SearchAnchor respects dividerColor property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor(
dividerColor: Colors.red,
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
final Finder findDivider = find.byType(Divider);
final Container dividerContainer = tester.widget<Container>(find.descendant(of: findDivider, matching: find.byType(Container)).first);
final BoxDecoration decoration = dividerContainer.decoration! as BoxDecoration;
expect(decoration.border!.bottom.color, Colors.red);
});
testWidgets('SearchAnchor respects viewConstraints property', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: Center(
child: SearchAnchor(
isFullScreen: false,
viewConstraints: BoxConstraints.tight(const Size(280.0, 390.0)),
builder: (BuildContext context, SearchController controller) {
return IconButton(icon: const Icon(Icons.search), onPressed: () {
controller.openView();
},);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
));
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
await tester.pumpAndSettle();
final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(sizedBox.width, 280.0);
expect(sizedBox.height, 390.0);
});
testWidgets('SearchAnchor respects builder property - LTR', (WidgetTester tester) async {
Widget buildAnchor({required SearchAnchorChildBuilder builder}) {
return MaterialApp(
home: Material(
child: Align(
alignment: Alignment.topCenter,
child: SearchAnchor(
isFullScreen: false,
builder: builder,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
);
}
await tester.pumpWidget(buildAnchor(
builder: (BuildContext context, SearchController controller)
=> const Icon(Icons.search)
));
final Rect anchorRect = tester.getRect(find.byIcon(Icons.search));
expect(anchorRect.size, const Size(24.0, 24.0));
expect(anchorRect, equals(const Rect.fromLTRB(388.0, 0.0, 412.0, 24.0)));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(searchViewRect, equals(const Rect.fromLTRB(388.0, 0.0, 748.0, 400.0)));
// Search view top left should be the same as the anchor top left
expect(searchViewRect.topLeft, anchorRect.topLeft);
});
testWidgets('SearchAnchor respects builder property - RTL', (WidgetTester tester) async {
Widget buildAnchor({required SearchAnchorChildBuilder builder}) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: Align(
alignment: Alignment.topCenter,
child: SearchAnchor(
isFullScreen: false,
builder: builder,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),
);
}
await tester.pumpWidget(buildAnchor(builder: (BuildContext context, SearchController controller)
=> const Icon(Icons.search)));
final Rect anchorRect = tester.getRect(find.byIcon(Icons.search));
expect(anchorRect.size, const Size(24.0, 24.0));
expect(anchorRect, equals(const Rect.fromLTRB(388.0, 0.0, 412.0, 24.0)));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(searchViewRect, equals(const Rect.fromLTRB(52.0, 0.0, 412.0, 400.0)));
// Search view top right should be the same as the anchor top right
expect(searchViewRect.topRight, anchorRect.topRight);
});
testWidgets('SearchAnchor respects suggestionsBuilder property', (WidgetTester tester) async {
final SearchController controller = SearchController();
const String suggestion = 'suggestion text';
await tester.pumpWidget(MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Align(
alignment: Alignment.topCenter,
child: SearchAnchor(
searchController: controller,
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[
ListTile(
title: const Text(suggestion),
onTap: () {
setState(() {
controller.closeView(suggestion);
});
}),
];
},
),
),
);
}
),
));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Finder listTile = find.widgetWithText(ListTile, suggestion);
expect(listTile, findsOneWidget);
await tester.tap(listTile);
await tester.pumpAndSettle();
expect(controller.isOpen, false);
expect(controller.value.text, suggestion);
});
testWidgets('SearchAnchor.bar has a default search bar as the anchor', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: Align(
alignment: Alignment.topLeft,
child: SearchAnchor.bar(
isFullScreen: false,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),),
);
expect(find.byType(SearchBar), findsOneWidget);
final Rect anchorRect = tester.getRect(find.byType(SearchBar));
expect(anchorRect.size, const Size(800.0, 56.0));
expect(anchorRect, equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 56.0)));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 400.0)));
// Search view has same width with the default anchor(search bar).
expect(searchViewRect.width, anchorRect.width);
});
testWidgets('SearchController can open/close view', (WidgetTester tester) async {
final SearchController controller = SearchController();
await tester.pumpWidget(MaterialApp(
home: Material(
child: SearchAnchor.bar(
searchController: controller,
isFullScreen: false,
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[
ListTile(
title: const Text('item 0'),
onTap: () {
controller.closeView('item 0');
},
)
];
},
),
),),
);
expect(controller.isOpen, false);
await tester.tap(find.byType(SearchBar));
await tester.pumpAndSettle();
expect(controller.isOpen, true);
await tester.tap(find.widgetWithText(ListTile, 'item 0'));
await tester.pumpAndSettle();
expect(controller.isOpen, false);
controller.openView();
expect(controller.isOpen, true);
});
}
TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
@ -779,3 +1479,12 @@ Future<TestGesture> _pointGestureToSearchBar(WidgetTester tester) async {
await gesture.moveTo(center);
return gesture;
}
Finder findViewContent() {
return find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_ViewContent';
});
}
Material getSearchViewMaterial(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: findViewContent(), matching: find.byType(Material)).first);
}

View File

@ -0,0 +1,245 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('SearchViewThemeData copyWith, ==, hashCode basics', () {
expect(const SearchViewThemeData(), const SearchViewThemeData().copyWith());
expect(const SearchViewThemeData().hashCode, const SearchViewThemeData()
.copyWith()
.hashCode);
});
test('SearchViewThemeData lerp special cases', () {
expect(SearchViewThemeData.lerp(null, null, 0), null);
const SearchViewThemeData data = SearchViewThemeData();
expect(identical(SearchViewThemeData.lerp(data, data, 0.5), data), true);
});
test('SearchViewThemeData defaults', () {
const SearchViewThemeData themeData = SearchViewThemeData();
expect(themeData.backgroundColor, null);
expect(themeData.elevation, null);
expect(themeData.surfaceTintColor, null);
expect(themeData.constraints, null);
expect(themeData.side, null);
expect(themeData.shape, null);
expect(themeData.headerTextStyle, null);
expect(themeData.headerHintStyle, null);
expect(themeData.dividerColor, null);
const SearchViewTheme theme = SearchViewTheme(data: SearchViewThemeData(), child: SizedBox());
expect(theme.data.backgroundColor, null);
expect(theme.data.elevation, null);
expect(theme.data.surfaceTintColor, null);
expect(theme.data.constraints, null);
expect(theme.data.side, null);
expect(theme.data.shape, null);
expect(theme.data.headerTextStyle, null);
expect(theme.data.headerHintStyle, null);
expect(theme.data.dividerColor, null);
});
testWidgets('Default SearchViewThemeData debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SearchViewThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('SearchViewThemeData implements debugFillProperties', (
WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SearchViewThemeData(
backgroundColor: Color(0xfffffff1),
elevation: 3.5,
surfaceTintColor: Color(0xfffffff3),
side: BorderSide(width: 2.5, color: Color(0xfffffff5)),
shape: RoundedRectangleBorder(),
headerTextStyle: TextStyle(fontSize: 24.0),
headerHintStyle: TextStyle(fontSize: 16.0),
constraints: BoxConstraints(minWidth: 350, minHeight: 240),
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description[0], 'backgroundColor: Color(0xfffffff1)');
expect(description[1], 'elevation: 3.5');
expect(description[2], 'surfaceTintColor: Color(0xfffffff3)');
expect(description[3], 'side: BorderSide(color: Color(0xfffffff5), width: 2.5)');
expect(description[4], 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)');
expect(description[5], 'headerTextStyle: TextStyle(inherit: true, size: 24.0)');
expect(description[6], 'headerHintStyle: TextStyle(inherit: true, size: 16.0)');
expect(description[7], 'constraints: BoxConstraints(350.0<=w<=Infinity, 240.0<=h<=Infinity)');
});
group('[Theme, SearchViewTheme, SearchView properties overrides]', () {
const Color backgroundColor = Color(0xff000001);
const double elevation = 5.0;
const Color surfaceTintColor = Color(0xff000002);
const BorderSide side = BorderSide(color: Color(0xff000003), width: 2.0);
const OutlinedBorder shape = RoundedRectangleBorder(side: side, borderRadius: BorderRadius.all(Radius.circular(20.0)));
const TextStyle headerTextStyle = TextStyle(color: Color(0xff000004), fontSize: 20.0);
const TextStyle headerHintStyle = TextStyle(color: Color(0xff000005), fontSize: 18.0);
const BoxConstraints constraints = BoxConstraints(minWidth: 250.0, maxWidth: 300.0, minHeight: 450.0);
const SearchViewThemeData searchViewTheme = SearchViewThemeData(
backgroundColor: backgroundColor,
elevation: elevation,
surfaceTintColor: surfaceTintColor,
side: side,
shape: shape,
headerTextStyle: headerTextStyle,
headerHintStyle: headerHintStyle,
constraints: constraints,
);
Widget buildFrame({
bool useSearchViewProperties = false,
SearchViewThemeData? searchViewThemeData,
SearchViewThemeData? overallTheme
}) {
final Widget child = Builder(
builder: (BuildContext context) {
if (!useSearchViewProperties) {
return SearchAnchor(
viewHintText: 'hint text',
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
isFullScreen: false,
);
}
return SearchAnchor(
viewHintText: 'hint text',
builder: (BuildContext context, SearchController controller) {
return const Icon(Icons.search);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
isFullScreen: false,
viewElevation: elevation,
viewBackgroundColor: backgroundColor,
viewSurfaceTintColor: surfaceTintColor,
viewSide: side,
viewShape: shape,
headerTextStyle: headerTextStyle,
headerHintStyle: headerHintStyle,
viewConstraints: constraints,
);
},
);
return MaterialApp(
theme: ThemeData.from(
colorScheme: const ColorScheme.light(), useMaterial3: true)
.copyWith(
searchViewTheme: overallTheme,
),
home: Scaffold(
body: Center(
// If the SearchViewThemeData widget is present, it's used
// instead of the Theme's ThemeData.searchViewTheme.
child: searchViewThemeData == null ? child : SearchViewTheme(
data: searchViewThemeData,
child: child,
),
),
),
);
}
Finder findViewContent() {
return find.byWidgetPredicate((Widget widget) {
return widget.runtimeType.toString() == '_ViewContent';
});
}
Material getSearchViewMaterial(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: findViewContent(), matching: find.byType(Material)).first);
}
Future<void> checkSearchView(WidgetTester tester) async {
final Material material = getSearchViewMaterial(tester);
expect(material.elevation, elevation);
expect(material.color, backgroundColor);
expect(material.surfaceTintColor, surfaceTintColor);
expect(material.shape, shape);
final SizedBox sizedBox = tester.widget<SizedBox>(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(sizedBox.width, 250.0);
expect(sizedBox.height, 450.0);
final Text hintText = tester.widget(find.text('hint text'));
expect(hintText.style?.color, headerHintStyle.color);
expect(hintText.style?.fontSize, headerHintStyle.fontSize);
await tester.enterText(find.byType(TextField), 'input');
final EditableText inputText = tester.widget(find.text('input'));
expect(inputText.style.color, headerTextStyle.color);
expect(inputText.style.fontSize, headerTextStyle.fontSize);
}
testWidgets('SearchView properties overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(useSearchViewProperties: true));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle(); // allow the animations to finish
checkSearchView(tester);
});
testWidgets('SearchView theme data overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(searchViewThemeData: searchViewTheme));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
checkSearchView(tester);
});
testWidgets('Overall Theme SearchView theme overrides defaults', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(overallTheme: searchViewTheme));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
checkSearchView(tester);
});
// Same as the previous tests with empty SearchViewThemeData's instead of null.
testWidgets('SearchView properties overrides defaults, empty theme and overall theme', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(useSearchViewProperties: true,
searchViewThemeData: const SearchViewThemeData(),
overallTheme: const SearchViewThemeData()));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle(); // allow the animations to finish
checkSearchView(tester);
});
testWidgets('SearchView theme overrides defaults and overall theme', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(searchViewThemeData: searchViewTheme,
overallTheme: const SearchViewThemeData()));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle(); // allow the animations to finish
checkSearchView(tester);
});
testWidgets('Overall Theme SearchView theme overrides defaults and null theme', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(overallTheme: searchViewTheme));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle(); // allow the animations to finish
checkSearchView(tester);
});
});
}

View File

@ -787,6 +787,7 @@ void main() {
progressIndicatorTheme: const ProgressIndicatorThemeData(),
radioTheme: const RadioThemeData(),
searchBarTheme: const SearchBarThemeData(),
searchViewTheme: const SearchViewThemeData(),
segmentedButtonTheme: const SegmentedButtonThemeData(),
sliderTheme: sliderTheme,
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.black),
@ -906,6 +907,7 @@ void main() {
progressIndicatorTheme: const ProgressIndicatorThemeData(),
radioTheme: const RadioThemeData(),
searchBarTheme: const SearchBarThemeData(),
searchViewTheme: const SearchViewThemeData(),
segmentedButtonTheme: const SegmentedButtonThemeData(),
sliderTheme: otherSliderTheme,
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.white),
@ -1010,6 +1012,7 @@ void main() {
progressIndicatorTheme: otherTheme.progressIndicatorTheme,
radioTheme: otherTheme.radioTheme,
searchBarTheme: otherTheme.searchBarTheme,
searchViewTheme: otherTheme.searchViewTheme,
sliderTheme: otherTheme.sliderTheme,
snackBarTheme: otherTheme.snackBarTheme,
switchTheme: otherTheme.switchTheme,
@ -1111,6 +1114,7 @@ void main() {
expect(themeDataCopy.progressIndicatorTheme, equals(otherTheme.progressIndicatorTheme));
expect(themeDataCopy.radioTheme, equals(otherTheme.radioTheme));
expect(themeDataCopy.searchBarTheme, equals(otherTheme.searchBarTheme));
expect(themeDataCopy.searchViewTheme, equals(otherTheme.searchViewTheme));
expect(themeDataCopy.sliderTheme, equals(otherTheme.sliderTheme));
expect(themeDataCopy.snackBarTheme, equals(otherTheme.snackBarTheme));
expect(themeDataCopy.switchTheme, equals(otherTheme.switchTheme));
@ -1249,6 +1253,7 @@ void main() {
'progressIndicatorTheme',
'radioTheme',
'searchBarTheme',
'searchViewTheme',
'segmentedButtonTheme',
'sliderTheme',
'snackBarTheme',