search_demo.dart 8.52 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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';

7 8
import '../../gallery/demo.dart';

9
class SearchDemo extends StatefulWidget {
10 11
  const SearchDemo({Key? key}) : super(key: key);

12 13 14
  static const String routeName = '/material/search';

  @override
15
  _SearchDemoState createState() => _SearchDemoState();
16 17 18
}

class _SearchDemoState extends State<SearchDemo> {
19 20
  final _SearchDemoSearchDelegate _delegate = _SearchDemoSearchDelegate();
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
21

22
  int? _lastIntegerSelected;
23 24 25

  @override
  Widget build(BuildContext context) {
26
    return Scaffold(
27
      key: _scaffoldKey,
28 29
      appBar: AppBar(
        leading: IconButton(
30
          tooltip: 'Navigation menu',
31
          icon: AnimatedIcon(
32 33 34 35 36
            icon: AnimatedIcons.menu_arrow,
            color: Colors.white,
            progress: _delegate.transitionAnimation,
          ),
          onPressed: () {
37
            _scaffoldKey.currentState!.openDrawer();
38 39 40 41
          },
        ),
        title: const Text('Numbers'),
        actions: <Widget>[
42
          IconButton(
43 44 45
            tooltip: 'Search',
            icon: const Icon(Icons.search),
            onPressed: () async {
46
              final int? selected = await showSearch<int?>(
47 48 49 50 51 52 53 54 55 56
                context: context,
                delegate: _delegate,
              );
              if (selected != null && selected != _lastIntegerSelected) {
                setState(() {
                  _lastIntegerSelected = selected;
                });
              }
            },
          ),
57
          MaterialDemoDocumentationButton(SearchDemo.routeName),
58
          IconButton(
59
            tooltip: 'More (not implemented)',
60
            icon: Icon(
61 62 63 64
              Theme.of(context).platform == TargetPlatform.iOS
                  ? Icons.more_horiz
                  : Icons.more_vert,
            ),
65
            onPressed: () { },
66 67 68
          ),
        ],
      ),
69 70
      body: Center(
        child: Column(
71 72
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
73 74
            MergeSemantics(
              child: Column(
75 76
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
77
                  Row(
78 79
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: const <Widget>[
80 81
                      Text('Press the '),
                      Tooltip(
82
                        message: 'search',
83
                        child: Icon(
84 85 86 87
                          Icons.search,
                          size: 18.0,
                        ),
                      ),
88
                      Text(' icon in the AppBar'),
89 90 91 92 93 94 95
                    ],
                  ),
                  const Text('and search for an integer between 0 and 100,000.'),
                ],
              ),
            ),
            const SizedBox(height: 64.0),
96
            Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE' }.'),
97 98 99
          ],
        ),
      ),
100
      floatingActionButton: FloatingActionButton.extended(
101 102
        tooltip: 'Back', // Tests depend on this label to exit the demo.
        onPressed: () {
103
          Navigator.of(context).pop();
104 105 106 107
        },
        label: const Text('Close demo'),
        icon: const Icon(Icons.close),
      ),
108 109
      drawer: Drawer(
        child: Column(
110 111
          children: <Widget>[
            const UserAccountsDrawerHeader(
112 113 114 115
              accountName: Text('Peter Widget'),
              accountEmail: Text('peter.widget@example.com'),
              currentAccountPicture: CircleAvatar(
                backgroundImage: AssetImage(
116
                  'people/square/peter.png',
117 118 119 120 121
                  package: 'flutter_gallery_assets',
                ),
              ),
              margin: EdgeInsets.zero,
            ),
122
            MediaQuery.removePadding(
123 124 125 126
              context: context,
              // DrawerHeader consumes top MediaQuery padding.
              removeTop: true,
              child: const ListTile(
127 128
                leading: Icon(Icons.payment),
                title: Text('Placeholder'),
129 130 131 132 133 134 135 136 137
              ),
            ),
          ],
        ),
      ),
    );
  }
}

138
class _SearchDemoSearchDelegate extends SearchDelegate<int?> {
139
  final List<int> _data = List<int>.generate(100001, (int i) => i).reversed.toList();
140 141 142 143
  final List<int> _history = <int>[42607, 85604, 66374, 44, 174];

  @override
  Widget buildLeading(BuildContext context) {
144
    return IconButton(
145
      tooltip: 'Back',
146
      icon: AnimatedIcon(
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
        icon: AnimatedIcons.menu_arrow,
        progress: transitionAnimation,
      ),
      onPressed: () {
        close(context, null);
      },
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) {

    final Iterable<int> suggestions = query.isEmpty
        ? _history
        : _data.where((int i) => '$i'.startsWith(query));

163
    return _SuggestionList(
164
      query: query,
165
      suggestions: suggestions.map<String>((int i) => '$i').toList(),
166 167 168 169 170 171 172 173 174
      onSelected: (String suggestion) {
        query = suggestion;
        showResults(context);
      },
    );
  }

  @override
  Widget buildResults(BuildContext context) {
175
    final int? searched = int.tryParse(query);
176
    if (searched == null || !_data.contains(searched)) {
177 178
      return Center(
        child: Text(
179 180 181 182 183 184
          '"$query"\n is not a valid integer between 0 and 100,000.\nTry again.',
          textAlign: TextAlign.center,
        ),
      );
    }

185
    return ListView(
186
      children: <Widget>[
187
        _ResultCard(
188 189 190 191
          title: 'This integer',
          integer: searched,
          searchDelegate: this,
        ),
192
        _ResultCard(
193 194 195 196
          title: 'Next integer',
          integer: searched + 1,
          searchDelegate: this,
        ),
197
        _ResultCard(
198 199 200 201 202 203 204 205 206 207 208
          title: 'Previous integer',
          integer: searched - 1,
          searchDelegate: this,
        ),
      ],
    );
  }

  @override
  List<Widget> buildActions(BuildContext context) {
    return <Widget>[
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
      if (query.isEmpty)
        IconButton(
          tooltip: 'Voice Search',
          icon: const Icon(Icons.mic),
          onPressed: () {
            query = 'TODO: implement voice input';
          },
        )
      else
        IconButton(
          tooltip: 'Clear',
          icon: const Icon(Icons.clear),
          onPressed: () {
            query = '';
            showSuggestions(context);
          },
        ),
226 227
    ];
  }
228 229 230 231 232 233

  @override
  PreferredSizeWidget buildBottom(BuildContext context) => const PreferredSize(
    preferredSize: Size.fromHeight(56.0),
    child: Text('Numbers'),
  );
234 235 236 237 238
}

class _ResultCard extends StatelessWidget {
  const _ResultCard({this.integer, this.title, this.searchDelegate});

239 240 241
  final int? integer;
  final String? title;
  final SearchDelegate<int?>? searchDelegate;
242 243 244 245

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
246
    return GestureDetector(
247
      onTap: () {
248
        searchDelegate!.close(context, integer);
249
      },
250 251
      child: Card(
        child: Padding(
252
          padding: const EdgeInsets.all(8.0),
253
          child: Column(
254
            children: <Widget>[
255
              Text(title!),
256
              Text(
257
                '$integer',
258
                style: theme.textTheme.headline5!.copyWith(fontSize: 72.0),
259 260 261 262 263 264 265 266 267 268 269 270
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _SuggestionList extends StatelessWidget {
  const _SuggestionList({this.suggestions, this.query, this.onSelected});

271 272 273
  final List<String>? suggestions;
  final String? query;
  final ValueChanged<String>? onSelected;
274 275 276 277

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
278
    return ListView.builder(
279
      itemCount: suggestions!.length,
280
      itemBuilder: (BuildContext context, int i) {
281
        final String suggestion = suggestions![i];
282
        return ListTile(
283
          leading: query!.isEmpty ? const Icon(Icons.history) : const Icon(null),
284 285
          title: RichText(
            text: TextSpan(
286 287
              text: suggestion.substring(0, query!.length),
              style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold),
288
              children: <TextSpan>[
289
                TextSpan(
290
                  text: suggestion.substring(query!.length),
291
                  style: theme.textTheme.subtitle1,
292 293 294 295 296
                ),
              ],
            ),
          ),
          onTap: () {
297
            onSelected!(suggestion);
298 299 300 301 302 303
          },
        );
      },
    );
  }
}