Add SearchAnchor.viewOnOpen and SearchAnchor.bar.onOpen (#164541)

Fix https://github.com/flutter/flutter/issues/160886

From the context of the issue, user wants to observe open/close events
for search view with SearchController.isOpen. However, I think that is
not a friendly approach/solution due to it relies on a
TextEditingController, it is a ValueNotifier which we implicitly
understand is dedicated to text input.

During the investigation, I found that `viewOnClose` works perfectly
because it is a callback from `_SearchViewRoute` (which is a PopupRoute
so it has didPush and didPop callbacks that we can leverage). `onClose`
was called on `didPop`, so we can solve this by creating a similar
`onOpen` then call it on `didPush`. Users then can implement both
callbacks for their needs.

In this PR:

- Propose adding SearchAnchor.viewOnOpen and `SearchAnchor.bar`.onOpen
- Improve documentation of SearchController, so that users will not be
confused with `SearchController.isOpen`

#### Demo (after the fix)


https://github.com/user-attachments/assets/7f30b831-e2d7-4a72-bc0f-35c858e3427b



## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Signed-off-by: huycozy <huy@nevercode.io>
This commit is contained in:
Huy 2025-03-20 17:22:23 +07:00 committed by GitHub
parent 95843ca334
commit e0676b47c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 110 additions and 0 deletions

View File

@ -147,6 +147,7 @@ class SearchAnchor extends StatefulWidget {
this.viewOnChanged,
this.viewOnSubmitted,
this.viewOnClose,
this.viewOnOpen,
required this.builder,
required this.suggestionsBuilder,
this.textInputAction,
@ -173,6 +174,7 @@ class SearchAnchor extends StatefulWidget {
ValueChanged<String>? onSubmitted,
ValueChanged<String>? onChanged,
VoidCallback? onClose,
VoidCallback? onOpen,
MaterialStateProperty<double?>? barElevation,
MaterialStateProperty<Color?>? barBackgroundColor,
MaterialStateProperty<Color?>? barOverlayColor,
@ -375,6 +377,9 @@ class SearchAnchor extends StatefulWidget {
/// Called when the search view is closed.
final VoidCallback? viewOnClose;
/// Called when the search view is opened.
final VoidCallback? viewOnOpen;
/// 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.
@ -465,6 +470,7 @@ class _SearchAnchorState extends State<SearchAnchor> {
viewOnChanged: widget.viewOnChanged,
viewOnSubmitted: widget.viewOnSubmitted,
viewOnClose: widget.viewOnClose,
viewOnOpen: widget.viewOnOpen,
viewLeading: widget.viewLeading,
viewTrailing: widget.viewTrailing,
viewHintText: widget.viewHintText,
@ -544,6 +550,7 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> {
this.viewOnChanged,
this.viewOnSubmitted,
this.viewOnClose,
this.viewOnOpen,
this.toggleVisibility,
this.textDirection,
this.viewBuilder,
@ -576,6 +583,7 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> {
final ValueChanged<String>? viewOnChanged;
final ValueChanged<String>? viewOnSubmitted;
final VoidCallback? viewOnClose;
final VoidCallback? viewOnOpen;
final ValueGetter<bool>? toggleVisibility;
final TextDirection? textDirection;
final ViewBuilder? viewBuilder;
@ -641,6 +649,7 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> {
updateViewConfig(anchorKey.currentContext!);
updateTweens(anchorKey.currentContext!);
toggleVisibility?.call();
viewOnOpen?.call();
return super.didPush();
}
@ -1193,6 +1202,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
ValueChanged<String>? onChanged,
ValueChanged<String>? onSubmitted,
VoidCallback? onClose,
VoidCallback? onOpen,
required super.suggestionsBuilder,
super.textInputAction,
super.keyboardType,
@ -1207,6 +1217,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
viewOnSubmitted: onSubmitted,
viewOnChanged: onChanged,
viewOnClose: onClose,
viewOnOpen: onOpen,
builder: (BuildContext context, SearchController controller) {
return SearchBar(
constraints: constraints,
@ -1248,6 +1259,9 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
/// with methods such as [openView] and [closeView]. It can also control the text in the
/// input field.
///
/// To observe open/close state changes of search view, provide
/// [SearchAnchor.viewOnOpen] and/or [SearchAnchor.viewOnClose] callbacks.
///
/// See also:
///
/// * [SearchAnchor], a widget that defines a region that opens a search view.

View File

@ -3911,6 +3911,102 @@ void main() {
// No exception.
});
testWidgets('SearchAnchor viewOnOpen is called when the search view is opened', (
WidgetTester tester,
) async {
String searchViewState = 'Idle';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SearchAnchor(
viewOnClose: () {
searchViewState = 'Closed';
},
viewOnOpen: () {
searchViewState = 'Opened';
},
builder: (BuildContext context, SearchController controller) {
return IconButton(
icon: const Icon(Icons.search),
onPressed: () {
controller.openView();
},
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return List<Widget>.generate(5, (int index) {
final String item = 'item $index';
return ListTile(
leading: const Icon(Icons.history),
title: Text(item),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
);
});
},
),
),
),
),
);
expect(find.byIcon(Icons.search), findsOneWidget);
// Open search view.
await tester.tap(find.byIcon(Icons.search));
await tester.pump();
expect(searchViewState, 'Opened');
// Pop search view route.
await tester.tap(find.backButton());
await tester.pump();
expect(searchViewState, 'Closed');
});
testWidgets('SearchAnchor.bar onOpen is called when the search view is opened', (
WidgetTester tester,
) async {
String searchViewState = 'Idle';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SearchAnchor.bar(
onClose: () {
searchViewState = 'Closed';
},
onOpen: () {
searchViewState = 'Opened';
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return List<Widget>.generate(5, (int index) {
final String item = 'item $index';
return ListTile(
leading: const Icon(Icons.history),
title: Text(item),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
);
});
},
),
),
),
),
);
expect(find.byIcon(Icons.search), findsOneWidget);
// Open search view.
await tester.tap(find.byIcon(Icons.search));
await tester.pump();
expect(searchViewState, 'Opened');
// Pop search view route.
await tester.tap(find.backButton());
await tester.pump();
expect(searchViewState, 'Closed');
});
}
Future<void> checkSearchBarDefaults(