blob: fbb4682c8ee7e94b19b1e9f7bb7b8bd3789e3740 [file] [log] [blame]
// 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:async';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'divider.dart';
import 'divider_theme.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'input_border.dart';
import 'input_decorator.dart';
import 'material.dart';
import 'material_state.dart';
import 'search_bar_theme.dart';
import 'search_view_theme.dart';
import 'text_field.dart';
import 'text_theme.dart';
import 'theme.dart';
import 'theme_data.dart';
const int _kOpenViewMilliseconds = 600;
const Duration _kOpenViewDuration = Duration(milliseconds: _kOpenViewMilliseconds);
const Duration _kAnchorFadeDuration = Duration(milliseconds: 150);
const Curve _kViewFadeOnInterval = Interval(0.0, 1/2);
const Curve _kViewIconsFadeOnInterval = Interval(1/6, 2/6);
const Curve _kViewDividerFadeOnInterval = Interval(0.0, 1/6);
const Curve _kViewListFadeOnInterval = Interval(133 / _kOpenViewMilliseconds, 233 / _kOpenViewMilliseconds);
/// Signature for a function that creates a [Widget] which is used to open a search view.
///
/// The `controller` callback provided to [SearchAnchor.builder] can be used
/// to open the search view and control the editable field on the view.
typedef SearchAnchorChildBuilder = Widget Function(BuildContext context, SearchController controller);
/// Signature for a function that creates a [Widget] to build the suggestion list
/// based on the input in the search bar.
///
/// The `controller` callback provided to [SearchAnchor.suggestionsBuilder] can be used
/// to close the search view and control the editable field on the view.
typedef SuggestionsBuilder = FutureOr<Iterable<Widget>> Function(BuildContext context, SearchController controller);
/// Signature for a function that creates a [Widget] to layout the suggestion list.
///
/// Parameter `suggestions` is the content list that this function wants to lay out.
typedef ViewBuilder = Widget Function(Iterable<Widget> suggestions);
/// Manages a "search view" route that allows the user to select one of the
/// suggested completions for a search query.
///
/// The search view's route can either be shown by creating a [SearchController]
/// and then calling [SearchController.openView] or by tapping on an anchor.
/// When the anchor is tapped or [SearchController.openView] is called, the search view either
/// grows to a specific size, or grows to fill the entire screen. By default,
/// the search view only shows full screen on mobile platforms. Use [SearchAnchor.isFullScreen]
/// to override the default setting.
///
/// The search view is usually opened by a [SearchBar], an [IconButton] or an [Icon].
/// If [builder] returns an Icon, or any un-tappable widgets, we don't have
/// to explicitly call [SearchController.openView].
///
/// The search view route will be popped if the window size is changed and the
/// search view route is not in full-screen mode. However, if the search view route
/// is in full-screen mode, changing the window size, such as rotating a mobile
/// device from portrait mode to landscape mode, will not close the search view.
///
/// {@tool dartpad}
/// This example shows how to use an IconButton to open a search view in a [SearchAnchor].
/// It also shows how to use [SearchController] to open or close the search view route.
///
/// ** See code in examples/api/lib/material/search_anchor/search_anchor.2.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how to set up a floating (or pinned) AppBar with a
/// [SearchAnchor] for a title.
///
/// ** See code in examples/api/lib/material/search_anchor/search_anchor.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how to fetch the search suggestions from a remote API.
///
/// ** See code in examples/api/lib/material/search_anchor/search_anchor.3.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example demonstrates fetching the search suggestions asynchronously and
/// debouncing network calls.
///
/// ** See code in examples/api/lib/material/search_anchor/search_anchor.4.dart **
/// {@end-tool}
///
/// See also:
///
/// * [SearchBar], a widget that defines a search bar.
/// * [SearchBarTheme], a widget that overrides the default configuration of a search bar.
/// * [SearchViewTheme], a widget that overrides the default configuration of a search view.
class SearchAnchor extends StatefulWidget {
/// Creates a const [SearchAnchor].
///
/// The [builder] and [suggestionsBuilder] arguments are required.
const SearchAnchor({
super.key,
this.isFullScreen,
this.searchController,
this.viewBuilder,
this.viewLeading,
this.viewTrailing,
this.viewHintText,
this.viewBackgroundColor,
this.viewElevation,
this.viewSurfaceTintColor,
this.viewSide,
this.viewShape,
this.headerTextStyle,
this.headerHintStyle,
this.dividerColor,
this.viewConstraints,
required this.builder,
required this.suggestionsBuilder,
});
/// Create a [SearchAnchor] that has a [SearchBar] which opens a search view.
///
/// All the barX parameters are used to customize the anchor. Similarly, all the
/// viewX parameters are used to override the view's defaults.
///
/// {@tool dartpad}
/// This example shows how to use a [SearchAnchor.bar] which uses a default search
/// bar to open a search view route.
///
/// ** See code in examples/api/lib/material/search_anchor/search_anchor.0.dart **
/// {@end-tool}
///
/// The [suggestionsBuilder] argument must not be null.
factory SearchAnchor.bar({
Widget? barLeading,
Iterable<Widget>? barTrailing,
String? barHintText,
GestureTapCallback? onTap,
MaterialStateProperty<double?>? barElevation,
MaterialStateProperty<Color?>? barBackgroundColor,
MaterialStateProperty<Color?>? barOverlayColor,
MaterialStateProperty<BorderSide?>? barSide,
MaterialStateProperty<OutlinedBorder?>? barShape,
MaterialStateProperty<EdgeInsetsGeometry?>? barPadding,
MaterialStateProperty<TextStyle?>? barTextStyle,
MaterialStateProperty<TextStyle?>? barHintStyle,
Widget? viewLeading,
Iterable<Widget>? viewTrailing,
String? viewHintText,
Color? viewBackgroundColor,
double? viewElevation,
BorderSide? viewSide,
OutlinedBorder? viewShape,
TextStyle? viewHeaderTextStyle,
TextStyle? viewHeaderHintStyle,
Color? dividerColor,
BoxConstraints? constraints,
BoxConstraints? viewConstraints,
bool? isFullScreen,
SearchController searchController,
required SuggestionsBuilder suggestionsBuilder
}) = _SearchAnchorWithSearchBar;
/// Whether the search view grows to fill the entire screen when the
/// [SearchAnchor] is tapped.
///
/// By default, the search view is full-screen on mobile devices. On other
/// platforms, the search view only grows to a specific size that is determined
/// by the anchor and the default size.
final bool? isFullScreen;
/// An optional controller that allows opening and closing of the search view from
/// other widgets.
///
/// If this is null, one internal search controller is created automatically
/// and it is used to open the search view when the user taps on the anchor.
final SearchController? searchController;
/// Optional callback to obtain a widget to lay out the suggestion list of the
/// search view.
///
/// Default view uses a [ListView] with a vertical scroll direction.
final ViewBuilder? viewBuilder;
/// An optional widget to display before the text input field when the search
/// view is open.
///
/// Typically the [viewLeading] widget is an [Icon] or an [IconButton].
///
/// Defaults to a back button which pops the view.
final Widget? viewLeading;
/// An optional widget list to display after the text input field when the search
/// view is open.
///
/// Typically the [viewTrailing] widget list only has one or two widgets.
///
/// Defaults to an icon button which clears the text in the input field.
final Iterable<Widget>? viewTrailing;
/// Text that is displayed when the search bar's input field is empty.
final String? viewHintText;
/// The search view's background fill color.
///
/// If null, the value of [SearchViewThemeData.backgroundColor] will be used.
/// If this is also null, then the default value is [ColorScheme.surface].
final Color? viewBackgroundColor;
/// The elevation of the search view's [Material].
///
/// If null, the value of [SearchViewThemeData.elevation] will be used. If this
/// is also null, then default value is 6.0.
final double? viewElevation;
/// The surface tint color of the search view's [Material].
///
/// See [Material.surfaceTintColor] for more details.
///
/// If null, the value of [SearchViewThemeData.surfaceTintColor] will be used.
/// If this is also null, then the default value is [ColorScheme.surfaceTint].
final Color? viewSurfaceTintColor;
/// The color and weight of the search view's outline.
///
/// This value is combined with [viewShape] to create a shape decorated
/// with an outline. This will be ignored if the view is full-screen.
///
/// If null, the value of [SearchViewThemeData.side] will be used. If this is
/// also null, the search view doesn't have a side by default.
final BorderSide? viewSide;
/// The shape of the search view's underlying [Material].
///
/// This shape is combined with [viewSide] to create a shape decorated
/// with an outline.
///
/// If null, the value of [SearchViewThemeData.shape] will be used.
/// If this is also null, then the default value is a rectangle shape for full-screen
/// mode and a [RoundedRectangleBorder] shape with a 28.0 radius otherwise.
final OutlinedBorder? viewShape;
/// The style to use for the text being edited on the search view.
///
/// If null, defaults to the `bodyLarge` text style from the current [Theme].
/// The default text color is [ColorScheme.onSurface].
final TextStyle? headerTextStyle;
/// The style to use for the [viewHintText] on the search view.
///
/// If null, the value of [SearchViewThemeData.headerHintStyle] will be used.
/// If this is also null, the value of [headerTextStyle] will be used. If this is also null,
/// defaults to the `bodyLarge` text style from the current [Theme]. The default
/// text color is [ColorScheme.onSurfaceVariant].
final TextStyle? headerHintStyle;
/// The color of the divider on the search view.
///
/// If this property is null, then [SearchViewThemeData.dividerColor] is used.
/// If that is also null, the default value is [ColorScheme.outline].
final Color? dividerColor;
/// Optional size constraints for the search view.
///
/// By default, the search view has the same width as the anchor and is 2/3
/// the height of the screen. If the width and height of the view are within
/// the [viewConstraints], the view will show its default size. Otherwise,
/// the size of the view will be constrained by this property.
///
/// If null, the value of [SearchViewThemeData.constraints] will be used. If
/// this is also null, then the constraints defaults to:
/// ```dart
/// const BoxConstraints(minWidth: 360.0, minHeight: 240.0)
/// ```
final BoxConstraints? viewConstraints;
/// Called to create a widget which can open a search view route when it is tapped.
///
/// The widget returned by this builder is faded out when it is tapped.
/// At the same time a search view route is faded in.
///
/// This must not be null.
final SearchAnchorChildBuilder builder;
/// Called to get the suggestion list for the search view.
///
/// By default, the list returned by this builder is laid out in a [ListView].
/// To get a different layout, use [viewBuilder] to override.
final SuggestionsBuilder suggestionsBuilder;
@override
State<SearchAnchor> createState() => _SearchAnchorState();
}
class _SearchAnchorState extends State<SearchAnchor> {
Size? _screenSize;
bool _anchorIsVisible = true;
final GlobalKey _anchorKey = GlobalKey();
bool get _viewIsOpen => !_anchorIsVisible;
late SearchController? _internalSearchController;
SearchController get _searchController => widget.searchController ?? _internalSearchController!;
@override
void initState() {
super.initState();
if (widget.searchController == null) {
_internalSearchController = SearchController();
}
_searchController._attach(this);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final Size updatedScreenSize = MediaQuery.of(context).size;
if (_screenSize != null && _screenSize != updatedScreenSize) {
if (_searchController.isOpen && !getShowFullScreenView()) {
_closeView(null);
}
}
_screenSize = updatedScreenSize;
}
@override
void dispose() {
super.dispose();
_searchController._detach(this);
_internalSearchController = null;
}
void _openView() {
Navigator.of(context).push(_SearchViewRoute(
viewLeading: widget.viewLeading,
viewTrailing: widget.viewTrailing,
viewHintText: widget.viewHintText,
viewBackgroundColor: widget.viewBackgroundColor,
viewElevation: widget.viewElevation,
viewSurfaceTintColor: widget.viewSurfaceTintColor,
viewSide: widget.viewSide,
viewShape: widget.viewShape,
viewHeaderTextStyle: widget.headerTextStyle,
viewHeaderHintStyle: widget.headerHintStyle,
dividerColor: widget.dividerColor,
viewConstraints: widget.viewConstraints,
showFullScreenView: getShowFullScreenView(),
toggleVisibility: toggleVisibility,
textDirection: Directionality.of(context),
viewBuilder: widget.viewBuilder,
anchorKey: _anchorKey,
searchController: _searchController,
suggestionsBuilder: widget.suggestionsBuilder,
));
}
void _closeView(String? selectedText) {
if (selectedText != null) {
_searchController.text = selectedText;
}
Navigator.of(context).pop();
}
bool toggleVisibility() {
setState(() {
_anchorIsVisible = !_anchorIsVisible;
});
return _anchorIsVisible;
}
bool getShowFullScreenView() {
if (widget.isFullScreen != null) {
return widget.isFullScreen!;
}
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return true;
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
return false;
}
}
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
key: _anchorKey,
opacity: _anchorIsVisible ? 1.0 : 0.0,
duration: _kAnchorFadeDuration,
child: GestureDetector(
onTap: _openView,
child: widget.builder(context, _searchController),
),
);
}
}
class _SearchViewRoute extends PopupRoute<_SearchViewRoute> {
_SearchViewRoute({
this.toggleVisibility,
this.textDirection,
this.viewBuilder,
this.viewLeading,
this.viewTrailing,
this.viewHintText,
this.viewBackgroundColor,
this.viewElevation,
this.viewSurfaceTintColor,
this.viewSide,
this.viewShape,
this.viewHeaderTextStyle,
this.viewHeaderHintStyle,
this.dividerColor,
this.viewConstraints,
required this.showFullScreenView,
required this.anchorKey,
required this.searchController,
required this.suggestionsBuilder,
});
final ValueGetter<bool>? toggleVisibility;
final TextDirection? textDirection;
final ViewBuilder? viewBuilder;
final Widget? viewLeading;
final Iterable<Widget>? viewTrailing;
final String? viewHintText;
final Color? viewBackgroundColor;
final double? viewElevation;
final Color? viewSurfaceTintColor;
final BorderSide? viewSide;
final OutlinedBorder? viewShape;
final TextStyle? viewHeaderTextStyle;
final TextStyle? viewHeaderHintStyle;
final Color? dividerColor;
final BoxConstraints? viewConstraints;
final bool showFullScreenView;
final GlobalKey anchorKey;
final SearchController searchController;
final SuggestionsBuilder suggestionsBuilder;
@override
Color? get barrierColor => Colors.transparent;
@override
bool get barrierDismissible => true;
@override
String? get barrierLabel => 'Dismiss';
late final SearchViewThemeData viewDefaults;
late final SearchViewThemeData viewTheme;
late final DividerThemeData dividerTheme;
final RectTween _rectTween = RectTween();
Rect? getRect() {
final BuildContext? context = anchorKey.currentContext;
if (context != null) {
final RenderBox searchBarBox = context.findRenderObject()! as RenderBox;
final Size boxSize = searchBarBox.size;
final NavigatorState navigator = Navigator.of(context);
final Offset boxLocation = searchBarBox.localToGlobal(Offset.zero, ancestor: navigator.context.findRenderObject());
return boxLocation & boxSize;
}
return null;
}
@override
TickerFuture didPush() {
assert(anchorKey.currentContext != null);
updateViewConfig(anchorKey.currentContext!);
updateTweens(anchorKey.currentContext!);
toggleVisibility?.call();
return super.didPush();
}
@override
bool didPop(_SearchViewRoute? result) {
assert(anchorKey.currentContext != null);
updateTweens(anchorKey.currentContext!);
toggleVisibility?.call();
return super.didPop(result);
}
void updateViewConfig(BuildContext context) {
viewDefaults = _SearchViewDefaultsM3(context, isFullScreen: showFullScreenView);
viewTheme = SearchViewTheme.of(context);
dividerTheme = DividerTheme.of(context);
}
void updateTweens(BuildContext context) {
final RenderBox navigator = Navigator.of(context).context.findRenderObject()! as RenderBox;
final Size screenSize = navigator.size;
final Rect anchorRect = getRect() ?? Rect.zero;
final BoxConstraints effectiveConstraints = viewConstraints ?? viewTheme.constraints ?? viewDefaults.constraints!;
_rectTween.begin = anchorRect;
final double viewWidth = clampDouble(anchorRect.width, effectiveConstraints.minWidth, effectiveConstraints.maxWidth);
final double viewHeight = clampDouble(screenSize.height * 2 / 3, effectiveConstraints.minHeight, effectiveConstraints.maxHeight);
switch (textDirection ?? TextDirection.ltr) {
case TextDirection.ltr:
final double viewLeftToScreenRight = screenSize.width - anchorRect.left;
final double viewTopToScreenBottom = screenSize.height - anchorRect.top;
// Make sure the search view doesn't go off the screen. If the search view
// doesn't fit, move the top-left corner of the view to fit the window.
// If the window is smaller than the view, then we resize the view to fit the window.
Offset topLeft = anchorRect.topLeft;
if (viewLeftToScreenRight < viewWidth) {
topLeft = Offset(screenSize.width - math.min(viewWidth, screenSize.width), topLeft.dy);
}
if (viewTopToScreenBottom < viewHeight) {
topLeft = Offset(topLeft.dx, screenSize.height - math.min(viewHeight, screenSize.height));
}
final Size endSize = Size(viewWidth, viewHeight);
_rectTween.end = showFullScreenView ? Offset.zero & screenSize : (topLeft & endSize);
return;
case TextDirection.rtl:
final double viewRightToScreenLeft = anchorRect.right;
final double viewTopToScreenBottom = screenSize.height - anchorRect.top;
// Make sure the search view doesn't go off the screen.
Offset topLeft = Offset(math.max(anchorRect.right - viewWidth, 0.0), anchorRect.top);
if (viewRightToScreenLeft < viewWidth) {
topLeft = Offset(0.0, topLeft.dy);
}
if (viewTopToScreenBottom < viewHeight) {
topLeft = Offset(topLeft.dx, screenSize.height - math.min(viewHeight, screenSize.height));
}
final Size endSize = Size(viewWidth, viewHeight);
_rectTween.end = showFullScreenView ? Offset.zero & screenSize : (topLeft & endSize);
}
}
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return Directionality(
textDirection: textDirection ?? TextDirection.ltr,
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
final Animation<double> curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.easeInOutCubicEmphasized,
reverseCurve: Curves.easeInOutCubicEmphasized.flipped,
);
final Rect viewRect = _rectTween.evaluate(curvedAnimation)!;
final double topPadding = showFullScreenView
? lerpDouble(0.0, MediaQuery.paddingOf(context).top, curvedAnimation.value)!
: 0.0;
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: _kViewFadeOnInterval,
reverseCurve: _kViewFadeOnInterval.flipped,
),
child: _ViewContent(
viewLeading: viewLeading,
viewTrailing: viewTrailing,
viewHintText: viewHintText,
viewBackgroundColor: viewBackgroundColor,
viewElevation: viewElevation,
viewSurfaceTintColor: viewSurfaceTintColor,
viewSide: viewSide,
viewShape: viewShape,
viewHeaderTextStyle: viewHeaderTextStyle,
viewHeaderHintStyle: viewHeaderHintStyle,
dividerColor: dividerColor,
showFullScreenView: showFullScreenView,
animation: curvedAnimation,
topPadding: topPadding,
viewMaxWidth: _rectTween.end!.width,
viewRect: viewRect,
viewDefaults: viewDefaults,
viewTheme: viewTheme,
dividerTheme: dividerTheme,
viewBuilder: viewBuilder,
searchController: searchController,
suggestionsBuilder: suggestionsBuilder,
),
);
}
),
);
}
@override
Duration get transitionDuration => _kOpenViewDuration;
}
class _ViewContent extends StatefulWidget {
const _ViewContent({
this.viewBuilder,
this.viewLeading,
this.viewTrailing,
this.viewHintText,
this.viewBackgroundColor,
this.viewElevation,
this.viewSurfaceTintColor,
this.viewSide,
this.viewShape,
this.viewHeaderTextStyle,
this.viewHeaderHintStyle,
this.dividerColor,
required this.showFullScreenView,
required this.topPadding,
required this.animation,
required this.viewMaxWidth,
required this.viewRect,
required this.viewDefaults,
required this.viewTheme,
required this.dividerTheme,
required this.searchController,
required this.suggestionsBuilder,
});
final ViewBuilder? viewBuilder;
final Widget? viewLeading;
final Iterable<Widget>? viewTrailing;
final String? viewHintText;
final Color? viewBackgroundColor;
final double? viewElevation;
final Color? viewSurfaceTintColor;
final BorderSide? viewSide;
final OutlinedBorder? viewShape;
final TextStyle? viewHeaderTextStyle;
final TextStyle? viewHeaderHintStyle;
final Color? dividerColor;
final bool showFullScreenView;
final double topPadding;
final Animation<double> animation;
final double viewMaxWidth;
final Rect viewRect;
final SearchViewThemeData viewDefaults;
final SearchViewThemeData viewTheme;
final DividerThemeData dividerTheme;
final SearchController searchController;
final SuggestionsBuilder suggestionsBuilder;
@override
State<_ViewContent> createState() => _ViewContentState();
}
class _ViewContentState extends State<_ViewContent> {
Size? _screenSize;
late Rect _viewRect;
late final SearchController _controller;
Iterable<Widget> result = <Widget>[];
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_viewRect = widget.viewRect;
_controller = widget.searchController;
if (!_focusNode.hasFocus) {
_focusNode.requestFocus();
}
}
@override
void didUpdateWidget(covariant _ViewContent oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.viewRect != oldWidget.viewRect) {
setState(() {
_viewRect = widget.viewRect;
});
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final Size updatedScreenSize = MediaQuery.of(context).size;
if (_screenSize != updatedScreenSize) {
_screenSize = updatedScreenSize;
if (widget.showFullScreenView) {
_viewRect = Offset.zero & _screenSize!;
}
}
unawaited(updateSuggestions());
}
Widget viewBuilder(Iterable<Widget> suggestions) {
if (widget.viewBuilder == null) {
return MediaQuery.removePadding(
context: context,
removeTop: true,
child: ListView(
children: suggestions.toList()
),
);
}
return widget.viewBuilder!(suggestions);
}
Future<void> updateSuggestions() async {
final Iterable<Widget> suggestions = await widget.suggestionsBuilder(context, _controller);
if (mounted) {
setState(() {
result = suggestions;
});
}
}
@override
Widget build(BuildContext context) {
final Widget defaultLeading = IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () { Navigator.of(context).pop(); },
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
);
final List<Widget> defaultTrailing = <Widget>[
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_controller.clear();
updateSuggestions();
},
),
];
final Color effectiveBackgroundColor = widget.viewBackgroundColor
?? widget.viewTheme.backgroundColor
?? widget.viewDefaults.backgroundColor!;
final Color effectiveSurfaceTint = widget.viewSurfaceTintColor
?? widget.viewTheme.surfaceTintColor
?? widget.viewDefaults.surfaceTintColor!;
final double effectiveElevation = widget.viewElevation
?? widget.viewTheme.elevation
?? widget.viewDefaults.elevation!;
final BorderSide? effectiveSide = widget.viewSide
?? widget.viewTheme.side
?? widget.viewDefaults.side;
OutlinedBorder effectiveShape = widget.viewShape
?? widget.viewTheme.shape
?? widget.viewDefaults.shape!;
if (effectiveSide != null) {
effectiveShape = effectiveShape.copyWith(side: effectiveSide);
}
final Color effectiveDividerColor = widget.dividerColor
?? widget.viewTheme.dividerColor
?? widget.dividerTheme.color
?? widget.viewDefaults.dividerColor!;
final TextStyle? effectiveTextStyle = widget.viewHeaderTextStyle
?? widget.viewTheme.headerTextStyle
?? widget.viewDefaults.headerTextStyle;
final TextStyle? effectiveHintStyle = widget.viewHeaderHintStyle
?? widget.viewTheme.headerHintStyle
?? widget.viewHeaderTextStyle
?? widget.viewTheme.headerTextStyle
?? widget.viewDefaults.headerHintStyle;
final Widget viewDivider = DividerTheme(
data: widget.dividerTheme.copyWith(color: effectiveDividerColor),
child: const Divider(height: 1),
);
return Align(
alignment: Alignment.topLeft,
child: Transform.translate(
offset: _viewRect.topLeft,
child: SizedBox(
width: _viewRect.width,
height: _viewRect.height,
child: Material(
clipBehavior: Clip.antiAlias,
shape: effectiveShape,
color: effectiveBackgroundColor,
surfaceTintColor: effectiveSurfaceTint,
elevation: effectiveElevation,
child: ClipRect(
clipBehavior: Clip.antiAlias,
child: OverflowBox(
alignment: Alignment.topLeft,
maxWidth: math.min(widget.viewMaxWidth, _screenSize!.width),
minWidth: 0,
child: FadeTransition(
opacity: CurvedAnimation(
parent: widget.animation,
curve: _kViewIconsFadeOnInterval,
reverseCurve: _kViewIconsFadeOnInterval.flipped,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: widget.topPadding),
child: SafeArea(
top: false,
bottom: false,
child: SearchBar(
constraints: widget.showFullScreenView ? BoxConstraints(minHeight: _SearchViewDefaultsM3.fullScreenBarHeight) : null,
focusNode: _focusNode,
leading: widget.viewLeading ?? defaultLeading,
trailing: widget.viewTrailing ?? defaultTrailing,
hintText: widget.viewHintText,
backgroundColor: const MaterialStatePropertyAll<Color>(Colors.transparent),
overlayColor: const MaterialStatePropertyAll<Color>(Colors.transparent),
elevation: const MaterialStatePropertyAll<double>(0.0),
textStyle: MaterialStatePropertyAll<TextStyle?>(effectiveTextStyle),
hintStyle: MaterialStatePropertyAll<TextStyle?>(effectiveHintStyle),
controller: _controller,
onChanged: (_) {
updateSuggestions();
},
),
),
),
FadeTransition(
opacity: CurvedAnimation(
parent: widget.animation,
curve: _kViewDividerFadeOnInterval,
reverseCurve: _kViewFadeOnInterval.flipped,
),
child: viewDivider),
Expanded(
child: FadeTransition(
opacity: CurvedAnimation(
parent: widget.animation,
curve: _kViewListFadeOnInterval,
reverseCurve: _kViewListFadeOnInterval.flipped,
),
child: viewBuilder(result),
),
),
],
),
),
),
),
),
),
),
);
}
}
class _SearchAnchorWithSearchBar extends SearchAnchor {
_SearchAnchorWithSearchBar({
Widget? barLeading,
Iterable<Widget>? barTrailing,
String? barHintText,
GestureTapCallback? onTap,
MaterialStateProperty<double?>? barElevation,
MaterialStateProperty<Color?>? barBackgroundColor,
MaterialStateProperty<Color?>? barOverlayColor,
MaterialStateProperty<BorderSide?>? barSide,
MaterialStateProperty<OutlinedBorder?>? barShape,
MaterialStateProperty<EdgeInsetsGeometry?>? barPadding,
MaterialStateProperty<TextStyle?>? barTextStyle,
MaterialStateProperty<TextStyle?>? barHintStyle,
super.viewLeading,
super.viewTrailing,
String? viewHintText,
super.viewBackgroundColor,
super.viewElevation,
super.viewSide,
super.viewShape,
TextStyle? viewHeaderTextStyle,
TextStyle? viewHeaderHintStyle,
super.dividerColor,
BoxConstraints? constraints,
super.viewConstraints,
super.isFullScreen,
super.searchController,
required super.suggestionsBuilder
}) : super(
viewHintText: viewHintText ?? barHintText,
headerTextStyle: viewHeaderTextStyle,
headerHintStyle: viewHeaderHintStyle,
builder: (BuildContext context, SearchController controller) {
return SearchBar(
constraints: constraints,
controller: controller,
onTap: () {
controller.openView();
onTap?.call();
},
onChanged: (_) {
controller.openView();
},
hintText: barHintText,
hintStyle: barHintStyle,
textStyle: barTextStyle,
elevation: barElevation,
backgroundColor: barBackgroundColor,
overlayColor: barOverlayColor,
side: barSide,
shape: barShape,
padding: barPadding ?? const MaterialStatePropertyAll<EdgeInsets>(EdgeInsets.symmetric(horizontal: 16.0)),
leading: barLeading ?? const Icon(Icons.search),
trailing: barTrailing,
);
}
);
}
/// A controller to manage a search view created by [SearchAnchor].
///
/// A [SearchController] is used to control a menu after it has been created,
/// with methods such as [openView] and [closeView]. It can also control the text in the
/// input field.
///
/// See also:
///
/// * [SearchAnchor], a widget that defines a region that opens a search view.
/// * [TextEditingController], A controller for an editable text field.
class SearchController extends TextEditingController {
// The anchor that this controller controls.
//
// This is set automatically when a [SearchController] is given to the anchor
// it controls.
_SearchAnchorState? _anchor;
/// Whether or not the associated search view is currently open.
bool get isOpen {
assert(_anchor != null);
return _anchor!._viewIsOpen;
}
/// Opens the search view that this controller is associated with.
void openView() {
assert(_anchor != null);
_anchor!._openView();
}
/// Close the search view that this search controller is associated with.
///
/// If `selectedText` is given, then the text value of the controller is set to
/// `selectedText`.
void closeView(String? selectedText) {
assert(_anchor != null);
_anchor!._closeView(selectedText);
}
// ignore: use_setters_to_change_properties
void _attach(_SearchAnchorState anchor) {
_anchor = anchor;
}
void _detach(_SearchAnchorState anchor) {
if (_anchor == anchor) {
_anchor = null;
}
}
}
/// A Material Design search bar.
///
/// A [SearchBar] looks like a [TextField]. Tapping a SearchBar typically shows a
/// "search view" route: a route with the search bar at the top and a list of
/// suggested completions for the search bar's text below. [SearchBar]s are
/// usually created by a [SearchAnchor.builder]. The builder provides a
/// [SearchController] that's used by the search bar's [SearchBar.onTap] or
/// [SearchBar.onChanged] callbacks to show the search view and to hide it
/// when the user selects a suggestion.
///
/// For [TextDirection.ltr], the [leading] widget is on the left side of the bar.
/// It should contain either a navigational action (such as a menu or up-arrow)
/// or a non-functional search icon.
///
/// The [trailing] is an optional list that appears at the other end of
/// the search bar. Typically only one or two action icons are included.
/// These actions can represent additional modes of searching (like voice search),
/// a separate high-level action (such as current location) or an overflow menu.
///
/// {@tool dartpad}
/// This example demonstrates how to use a [SearchBar] as the return value of the
/// [SearchAnchor.builder] property. The [SearchBar] also includes a leading search
/// icon and a trailing action to toggle the brightness.
///
/// ** See code in examples/api/lib/material/search_anchor/search_bar.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [SearchAnchor], a widget that typically uses an [IconButton] or a [SearchBar]
/// to manage a "search view" route.
/// * [SearchBarTheme], a widget that overrides the default configuration of a search bar.
/// * [SearchViewTheme], a widget that overrides the default configuration of a search view.
class SearchBar extends StatefulWidget {
/// Creates a Material Design search bar.
const SearchBar({
super.key,
this.controller,
this.focusNode,
this.hintText,
this.leading,
this.trailing,
this.onTap,
this.onChanged,
this.onSubmitted,
this.constraints,
this.elevation,
this.backgroundColor,
this.shadowColor,
this.surfaceTintColor,
this.overlayColor,
this.side,
this.shape,
this.padding,
this.textStyle,
this.hintStyle,
});
/// Controls the text being edited in the search bar's text field.
///
/// If null, this widget will create its own [TextEditingController].
final TextEditingController? controller;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// Text that suggests what sort of input the field accepts.
///
/// Displayed at the same location on the screen where text may be entered
/// when the input is empty.
///
/// Defaults to null.
final String? hintText;
/// A widget to display before the text input field.
///
/// Typically the [leading] widget is an [Icon] or an [IconButton].
final Widget? leading;
/// A list of Widgets to display in a row after the text field.
///
/// Typically these actions can represent additional modes of searching
/// (like voice search), an avatar, a separate high-level action (such as
/// current location) or an overflow menu. There should not be more than
/// two trailing actions.
final Iterable<Widget>? trailing;
/// Called when the user taps this search bar.
final GestureTapCallback? onTap;
/// Invoked upon user input.
final ValueChanged<String>? onChanged;
/// Called when the user indicates that they are done editing the text in the
/// field.
final ValueChanged<String>? onSubmitted;
/// Optional size constraints for the search bar.
///
/// If null, the value of [SearchBarThemeData.constraints] will be used. If
/// this is also null, then the constraints defaults to:
/// ```dart
/// const BoxConstraints(minWidth: 360.0, maxWidth: 800.0, minHeight: 56.0)
/// ```
final BoxConstraints? constraints;
/// The elevation of the search bar's [Material].
///
/// If null, the value of [SearchBarThemeData.elevation] will be used. If this
/// is also null, then default value is 6.0.
final MaterialStateProperty<double?>? elevation;
/// The search bar's background fill color.
///
/// If null, the value of [SearchBarThemeData.backgroundColor] will be used.
/// If this is also null, then the default value is [ColorScheme.surface].
final MaterialStateProperty<Color?>? backgroundColor;
/// The shadow color of the search bar's [Material].
///
/// If null, the value of [SearchBarThemeData.shadowColor] will be used.
/// If this is also null, then the default value is [ColorScheme.shadow].
final MaterialStateProperty<Color?>? shadowColor;
/// The surface tint color of the search bar's [Material].
///
/// See [Material.surfaceTintColor] for more details.
///
/// If null, the value of [SearchBarThemeData.surfaceTintColor] will be used.
/// If this is also null, then the default value is [ColorScheme.surfaceTint].
final MaterialStateProperty<Color?>? surfaceTintColor;
/// The highlight color that's typically used to indicate that
/// the search bar is focused, hovered, or pressed.
final MaterialStateProperty<Color?>? overlayColor;
/// The color and weight of the search bar's outline.
///
/// This value is combined with [shape] to create a shape decorated
/// with an outline.
///
/// If null, the value of [SearchBarThemeData.side] will be used. If this is
/// also null, the search bar doesn't have a side by default.
final MaterialStateProperty<BorderSide?>? side;
/// The shape of the search bar's underlying [Material].
///
/// This shape is combined with [side] to create a shape decorated
/// with an outline.
///
/// If null, the value of [SearchBarThemeData.shape] will be used.
/// If this is also null, defaults to [StadiumBorder].
final MaterialStateProperty<OutlinedBorder?>? shape;
/// The padding between the search bar's boundary and its contents.
///
/// If null, the value of [SearchBarThemeData.padding] will be used.
/// If this is also null, then the default value is 16.0 horizontally.
final MaterialStateProperty<EdgeInsetsGeometry?>? padding;
/// The style to use for the text being edited.
///
/// If null, defaults to the `bodyLarge` text style from the current [Theme].
/// The default text color is [ColorScheme.onSurface].
final MaterialStateProperty<TextStyle?>? textStyle;
/// The style to use for the [hintText].
///
/// If null, the value of [SearchBarThemeData.hintStyle] will be used. If this
/// is also null, the value of [textStyle] will be used. If this is also null,
/// defaults to the `bodyLarge` text style from the current [Theme].
/// The default text color is [ColorScheme.onSurfaceVariant].
final MaterialStateProperty<TextStyle?>? hintStyle;
@override
State<SearchBar> createState() => _SearchBarState();
}
class _SearchBarState extends State<SearchBar> {
late final MaterialStatesController _internalStatesController;
late final FocusNode _focusNode;
@override
void initState() {
super.initState();
_internalStatesController = MaterialStatesController();
_internalStatesController.addListener(() {
setState(() {});
});
_focusNode = widget.focusNode ?? FocusNode();
}
@override
void dispose() {
_internalStatesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final TextDirection textDirection = Directionality.of(context);
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final IconThemeData iconTheme = IconTheme.of(context);
final SearchBarThemeData searchBarTheme = SearchBarTheme.of(context);
final SearchBarThemeData defaults = _SearchBarDefaultsM3(context);
T? resolve<T>(
MaterialStateProperty<T>? widgetValue,
MaterialStateProperty<T>? themeValue,
MaterialStateProperty<T>? defaultValue,
) {
final Set<MaterialState> states = _internalStatesController.value;
return widgetValue?.resolve(states) ?? themeValue?.resolve(states) ?? defaultValue?.resolve(states);
}
final TextStyle? effectiveTextStyle = resolve<TextStyle?>(widget.textStyle, searchBarTheme.textStyle, defaults.textStyle);
final double? effectiveElevation = resolve<double?>(widget.elevation, searchBarTheme.elevation, defaults.elevation);
final Color? effectiveShadowColor = resolve<Color?>(widget.shadowColor, searchBarTheme.shadowColor, defaults.shadowColor);
final Color? effectiveBackgroundColor = resolve<Color?>(widget.backgroundColor, searchBarTheme.backgroundColor, defaults.backgroundColor);
final Color? effectiveSurfaceTintColor = resolve<Color?>(widget.surfaceTintColor, searchBarTheme.surfaceTintColor, defaults.surfaceTintColor);
final OutlinedBorder? effectiveShape = resolve<OutlinedBorder?>(widget.shape, searchBarTheme.shape, defaults.shape);
final BorderSide? effectiveSide = resolve<BorderSide?>(widget.side, searchBarTheme.side, defaults.side);
final EdgeInsetsGeometry? effectivePadding = resolve<EdgeInsetsGeometry?>(widget.padding, searchBarTheme.padding, defaults.padding);
final MaterialStateProperty<Color?>? effectiveOverlayColor = widget.overlayColor ?? searchBarTheme.overlayColor ?? defaults.overlayColor;
final Set<MaterialState> states = _internalStatesController.value;
final TextStyle? effectiveHintStyle = widget.hintStyle?.resolve(states)
?? searchBarTheme.hintStyle?.resolve(states)
?? widget.textStyle?.resolve(states)
?? searchBarTheme.textStyle?.resolve(states)
?? defaults.hintStyle?.resolve(states);
final bool isDark = Theme.of(context).brightness == Brightness.dark;
bool isIconThemeColorDefault(Color? color) {
if (isDark) {
return color == kDefaultIconLightColor;
}
return color == kDefaultIconDarkColor;
}
Widget? leading;
if (widget.leading != null) {
leading = IconTheme.merge(
data: isIconThemeColorDefault(iconTheme.color)
? IconThemeData(color: colorScheme.onSurface)
: iconTheme,
child: widget.leading!,
);
}
List<Widget>? trailing;
if (widget.trailing != null) {
trailing = widget.trailing?.map((Widget trailing) => IconTheme.merge(
data: isIconThemeColorDefault(iconTheme.color)
? IconThemeData(color: colorScheme.onSurfaceVariant)
: iconTheme,
child: trailing,
)).toList();
}
return ConstrainedBox(
constraints: widget.constraints ?? searchBarTheme.constraints ?? defaults.constraints!,
child: Material(
elevation: effectiveElevation!,
shadowColor: effectiveShadowColor,
color: effectiveBackgroundColor,
surfaceTintColor: effectiveSurfaceTintColor,
shape: effectiveShape?.copyWith(side: effectiveSide),
child: InkWell(
onTap: () {
widget.onTap?.call();
_focusNode.requestFocus();
},
overlayColor: effectiveOverlayColor,
customBorder: effectiveShape?.copyWith(side: effectiveSide),
statesController: _internalStatesController,
child: Padding(
padding: effectivePadding!,
child: Row(
textDirection: textDirection,
children: <Widget>[
if (leading != null) leading,
Expanded(
child: IgnorePointer(
child: Padding(
padding: effectivePadding,
child: TextField(
focusNode: _focusNode,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
controller: widget.controller,
style: effectiveTextStyle,
decoration: InputDecoration(
hintText: widget.hintText,
).applyDefaults(InputDecorationTheme(
hintStyle: effectiveHintStyle,
// The configuration below is to make sure that the text field
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
enabledBorder: InputBorder.none,
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
// Setting `isDense` to true to allow the text field height to be
// smaller than 48.0
isDense: true,
)),
),
),
)
),
if (trailing != null) ...trailing,
],
),
),
),
),
);
}
}
// BEGIN GENERATED TOKEN PROPERTIES - SearchBar
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
class _SearchBarDefaultsM3 extends SearchBarThemeData {
_SearchBarDefaultsM3(this.context);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
@override
MaterialStateProperty<Color?>? get backgroundColor =>
MaterialStatePropertyAll<Color>(_colors.surface);
@override
MaterialStateProperty<double>? get elevation =>
const MaterialStatePropertyAll<double>(6.0);
@override
MaterialStateProperty<Color>? get shadowColor =>
MaterialStatePropertyAll<Color>(_colors.shadow);
@override
MaterialStateProperty<Color>? get surfaceTintColor =>
MaterialStatePropertyAll<Color>(_colors.surfaceTint);
@override
MaterialStateProperty<Color?>? get overlayColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface.withOpacity(0.12);
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return Colors.transparent;
}
return Colors.transparent;
});
// No default side
@override
MaterialStateProperty<OutlinedBorder>? get shape =>
const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder());
@override
MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
const MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.symmetric(horizontal: 8.0));
@override
MaterialStateProperty<TextStyle?> get textStyle =>
MaterialStatePropertyAll<TextStyle?>(_textTheme.bodyLarge?.copyWith(color: _colors.onSurface));
@override
MaterialStateProperty<TextStyle?> get hintStyle =>
MaterialStatePropertyAll<TextStyle?>(_textTheme.bodyLarge?.copyWith(color: _colors.onSurfaceVariant));
@override
BoxConstraints get constraints =>
const BoxConstraints(minWidth: 360.0, maxWidth: 800.0, minHeight: 56.0);
}
// END GENERATED TOKEN PROPERTIES - SearchBar
// BEGIN GENERATED TOKEN PROPERTIES - SearchView
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
class _SearchViewDefaultsM3 extends SearchViewThemeData {
_SearchViewDefaultsM3(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 = 72.0;
@override
Color? get backgroundColor => _colors.surface;
@override
double? get elevation => 6.0;
@override
Color? get surfaceTintColor => _colors.surfaceTint;
// No default side
@override
OutlinedBorder? get shape => isFullScreen
? const RoundedRectangleBorder()
: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0)));
@override
TextStyle? get headerTextStyle => _textTheme.bodyLarge?.copyWith(color: _colors.onSurface);
@override
TextStyle? get headerHintStyle => _textTheme.bodyLarge?.copyWith(color: _colors.onSurfaceVariant);
@override
BoxConstraints get constraints => const BoxConstraints(minWidth: 360.0, minHeight: 240.0);
@override
Color? get dividerColor => _colors.outline;
}
// END GENERATED TOKEN PROPERTIES - SearchView