mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Create SearchAnchor
and SearchViewTheme
Widget (#123256)
This commit is contained in:
parent
9a7387c72e
commit
0300cfa603
@ -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();
|
||||
|
54
dev/tools/gen_defaults/lib/search_view_template.dart
Normal file
54
dev/tools/gen_defaults/lib/search_view_template.dart
Normal 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')};
|
||||
}
|
||||
''';
|
||||
}
|
124
examples/api/lib/material/search_anchor/search_anchor.0.dart
Normal file
124
examples/api/lib/material/search_anchor/search_anchor.0.dart
Normal 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;
|
||||
}
|
87
examples/api/lib/material/search_anchor/search_anchor.1.dart
Normal file
87
examples/api/lib/material/search_anchor/search_anchor.1.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
65
examples/api/lib/material/search_anchor/search_anchor.2.dart
Normal file
65
examples/api/lib/material/search_anchor/search_anchor.2.dart
Normal 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}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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
217
packages/flutter/lib/src/material/search_view_theme.dart
Normal file
217
packages/flutter/lib/src/material/search_view_theme.dart
Normal 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;
|
||||
}
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
|
245
packages/flutter/test/material/search_view_theme_test.dart
Normal file
245
packages/flutter/test/material/search_view_theme_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user