Skip to content

Commit d5a1034

Browse files
authored
Expandable Search (flutter#17629)
1 parent 711174a commit d5a1034

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1412
-57
lines changed

examples/flutter_gallery/lib/demo/material/material.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export 'page_selector_demo.dart';
2424
export 'persistent_bottom_sheet_demo.dart';
2525
export 'progress_indicator_demo.dart';
2626
export 'scrollable_tabs_demo.dart';
27+
export 'search_demo.dart';
2728
export 'selection_controls_demo.dart';
2829
export 'slider_demo.dart';
2930
export 'snack_bar_demo.dart';
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
// Copyright 2018 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
class SearchDemo extends StatefulWidget {
8+
static const String routeName = '/material/search';
9+
10+
@override
11+
_SearchDemoState createState() => new _SearchDemoState();
12+
}
13+
14+
class _SearchDemoState extends State<SearchDemo> {
15+
final _SearchDemoSearchDelegate _delegate = new _SearchDemoSearchDelegate();
16+
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
17+
18+
int _lastIntegerSelected;
19+
20+
@override
21+
Widget build(BuildContext context) {
22+
return new Scaffold(
23+
key: _scaffoldKey,
24+
appBar: new AppBar(
25+
leading: new IconButton(
26+
tooltip: 'Navigation menu',
27+
icon: new AnimatedIcon(
28+
icon: AnimatedIcons.menu_arrow,
29+
color: Colors.white,
30+
progress: _delegate.transitionAnimation,
31+
),
32+
onPressed: () {
33+
_scaffoldKey.currentState.openDrawer();
34+
},
35+
),
36+
title: const Text('Numbers'),
37+
actions: <Widget>[
38+
new IconButton(
39+
tooltip: 'Search',
40+
icon: const Icon(Icons.search),
41+
onPressed: () async {
42+
final int selected = await showSearch<int>(
43+
context: context,
44+
delegate: _delegate,
45+
);
46+
if (selected != null && selected != _lastIntegerSelected) {
47+
setState(() {
48+
_lastIntegerSelected = selected;
49+
});
50+
}
51+
},
52+
),
53+
new IconButton(
54+
tooltip: 'More (not implemented)',
55+
icon: const Icon(Icons.more_vert),
56+
onPressed: () {},
57+
),
58+
],
59+
),
60+
body: new Center(
61+
child: new Column(
62+
mainAxisAlignment: MainAxisAlignment.center,
63+
children: <Widget>[
64+
new MergeSemantics(
65+
child: new Column(
66+
mainAxisAlignment: MainAxisAlignment.center,
67+
children: <Widget>[
68+
new Row(
69+
mainAxisAlignment: MainAxisAlignment.center,
70+
children: const <Widget>[
71+
const Text('Press the '),
72+
const Tooltip(
73+
message: 'search',
74+
child: const Icon(
75+
Icons.search,
76+
size: 18.0,
77+
),
78+
),
79+
const Text(' icon in the AppBar'),
80+
],
81+
),
82+
const Text('and search for an integer between 0 and 100,000.'),
83+
],
84+
),
85+
),
86+
const SizedBox(height: 64.0),
87+
new Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE' }.')
88+
],
89+
),
90+
),
91+
floatingActionButton: new FloatingActionButton.extended(
92+
tooltip: 'Back', // Tests depend on this label to exit the demo.
93+
onPressed: () {
94+
Navigator.of(context).pop();
95+
},
96+
label: const Text('Close demo'),
97+
icon: const Icon(Icons.close),
98+
),
99+
drawer: new Drawer(
100+
child: new Column(
101+
children: <Widget>[
102+
const UserAccountsDrawerHeader(
103+
accountName: const Text('Zach Widget'),
104+
accountEmail: const Text('[email protected]'),
105+
currentAccountPicture: const CircleAvatar(
106+
backgroundImage: const AssetImage(
107+
'shrine/vendors/zach.jpg',
108+
package: 'flutter_gallery_assets',
109+
),
110+
),
111+
margin: EdgeInsets.zero,
112+
),
113+
new MediaQuery.removePadding(
114+
context: context,
115+
// DrawerHeader consumes top MediaQuery padding.
116+
removeTop: true,
117+
child: const ListTile(
118+
leading: const Icon(Icons.payment),
119+
title: const Text('Placeholder'),
120+
),
121+
),
122+
],
123+
),
124+
),
125+
);
126+
}
127+
}
128+
129+
class _SearchDemoSearchDelegate extends SearchDelegate<int> {
130+
final List<int> _data = new List<int>.generate(100001, (int i) => i).reversed.toList();
131+
final List<int> _history = <int>[42607, 85604, 66374, 44, 174];
132+
133+
@override
134+
Widget buildLeading(BuildContext context) {
135+
return new IconButton(
136+
tooltip: 'Back',
137+
icon: new AnimatedIcon(
138+
icon: AnimatedIcons.menu_arrow,
139+
progress: transitionAnimation,
140+
),
141+
onPressed: () {
142+
close(context, null);
143+
},
144+
);
145+
}
146+
147+
@override
148+
Widget buildSuggestions(BuildContext context) {
149+
150+
final Iterable<int> suggestions = query.isEmpty
151+
? _history
152+
: _data.where((int i) => '$i'.startsWith(query));
153+
154+
return new _SuggestionList(
155+
query: query,
156+
suggestions: suggestions.map((int i) => '$i').toList(),
157+
onSelected: (String suggestion) {
158+
query = suggestion;
159+
showResults(context);
160+
},
161+
);
162+
}
163+
164+
@override
165+
Widget buildResults(BuildContext context) {
166+
final int searched = int.tryParse(query);
167+
if (searched == null || !_data.contains(searched)) {
168+
return new Center(
169+
child: new Text(
170+
'"$query"\n is not a valid integer between 0 and 100,000.\nTry again.',
171+
textAlign: TextAlign.center,
172+
),
173+
);
174+
}
175+
176+
return new ListView(
177+
children: <Widget>[
178+
new _ResultCard(
179+
title: 'This integer',
180+
integer: searched,
181+
searchDelegate: this,
182+
),
183+
new _ResultCard(
184+
title: 'Next integer',
185+
integer: searched + 1,
186+
searchDelegate: this,
187+
),
188+
new _ResultCard(
189+
title: 'Previous integer',
190+
integer: searched - 1,
191+
searchDelegate: this,
192+
),
193+
],
194+
);
195+
}
196+
197+
@override
198+
List<Widget> buildActions(BuildContext context) {
199+
return <Widget>[
200+
query.isEmpty
201+
? new IconButton(
202+
tooltip: 'Voice Search',
203+
icon: const Icon(Icons.mic),
204+
onPressed: () {
205+
query = 'TODO: implement voice input';
206+
},
207+
)
208+
: new IconButton(
209+
tooltip: 'Clear',
210+
icon: const Icon(Icons.clear),
211+
onPressed: () {
212+
query = '';
213+
showSuggestions(context);
214+
},
215+
)
216+
];
217+
}
218+
}
219+
220+
class _ResultCard extends StatelessWidget {
221+
const _ResultCard({this.integer, this.title, this.searchDelegate});
222+
223+
final int integer;
224+
final String title;
225+
final SearchDelegate<int> searchDelegate;
226+
227+
@override
228+
Widget build(BuildContext context) {
229+
final ThemeData theme = Theme.of(context);
230+
return new GestureDetector(
231+
onTap: () {
232+
searchDelegate.close(context, integer);
233+
},
234+
child: new Card(
235+
child: new Padding(
236+
padding: const EdgeInsets.all(8.0),
237+
child: new Column(
238+
children: <Widget>[
239+
new Text(title),
240+
new Text(
241+
'$integer',
242+
style: theme.textTheme.headline.copyWith(fontSize: 72.0),
243+
),
244+
],
245+
),
246+
),
247+
),
248+
);
249+
}
250+
}
251+
252+
class _SuggestionList extends StatelessWidget {
253+
const _SuggestionList({this.suggestions, this.query, this.onSelected});
254+
255+
final List<String> suggestions;
256+
final String query;
257+
final ValueChanged<String> onSelected;
258+
259+
@override
260+
Widget build(BuildContext context) {
261+
final ThemeData theme = Theme.of(context);
262+
return new ListView.builder(
263+
itemCount: suggestions.length,
264+
itemBuilder: (BuildContext context, int i) {
265+
final String suggestion = suggestions[i];
266+
return new ListTile(
267+
leading: query.isEmpty ? const Icon(Icons.history) : const Icon(null),
268+
title: new RichText(
269+
text: new TextSpan(
270+
text: suggestion.substring(0, query.length),
271+
style: theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold),
272+
children: <TextSpan>[
273+
new TextSpan(
274+
text: suggestion.substring(query.length),
275+
style: theme.textTheme.subhead,
276+
),
277+
],
278+
),
279+
),
280+
onTap: () {
281+
onSelected(suggestion);
282+
},
283+
);
284+
},
285+
);
286+
}
287+
}

examples/flutter_gallery/lib/gallery/demos.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,14 @@ List<GalleryDemo> _buildGalleryDemos() {
313313
routeName: OverscrollDemo.routeName,
314314
buildRoute: (BuildContext context) => const OverscrollDemo(),
315315
),
316+
new GalleryDemo(
317+
title: 'Search',
318+
subtitle: 'Expandable search',
319+
icon: Icons.search,
320+
category: _kMaterialComponents,
321+
routeName: SearchDemo.routeName,
322+
buildRoute: (BuildContext context) => new SearchDemo(),
323+
),
316324
new GalleryDemo(
317325
title: 'Selection controls',
318326
subtitle: 'Checkboxes, radio buttons, and switches',

packages/flutter/lib/material.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export 'src/material/raised_button.dart';
7878
export 'src/material/refresh_indicator.dart';
7979
export 'src/material/scaffold.dart';
8080
export 'src/material/scrollbar.dart';
81+
export 'src/material/search.dart';
8182
export 'src/material/shadows.dart';
8283
export 'src/material/slider.dart';
8384
export 'src/material/slider_theme.dart';

packages/flutter/lib/src/material/material_localizations.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ abstract class MaterialLocalizations {
160160
/// alert dialog widget is opened.
161161
String get alertDialogLabel;
162162

163+
/// Label indicating that a text field is a search field. This will be used
164+
/// as a hint text in the text field.
165+
String get searchFieldLabel;
166+
163167
/// The format used to lay out the time picker.
164168
///
165169
/// The documentation for [TimeOfDayFormat] enum values provides details on
@@ -537,6 +541,9 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
537541
@override
538542
String get alertDialogLabel => 'Alert';
539543

544+
@override
545+
String get searchFieldLabel => 'Search';
546+
540547
@override
541548
String aboutListTileTitle(String applicationName) => 'About $applicationName';
542549

0 commit comments

Comments
 (0)