Flutter - 实现 listView 搜索功能

Flutter - Implementing a listView search feature

我一直在尝试在我的应用程序中实现一个搜索栏,以便将选定的 listView 项目置于列表顶部。该列表包含相当多的项目,大约 1700 项,因此添加搜索栏是必不可少的。我希望 listView 搜索框显示在顶部 appBar 右侧的 search 图标中。下面是当前视图的图片以供参考。

当您单击搜索 iconButton 时,搜索字段应替换 appBar 中的标题。用户很明显这是针对加密 listView 的,因为我将在搜索视图中添加一个提示来识别它。

我没有包括我的所有代码,因为这对于堆栈问题来说会很麻烦,但下面是我的 home_page.dart 文件,我的 类 的其余部分用于底部加密 listView 可以在 this GitHub repo.

找到

这就是我的“home_page.dart”的样子;

import 'package:cryptick/cryptoData/crypto_data.dart';
import 'package:cryptick/cryptoData/trending_data.dart';
import 'package:cryptick/modules/crypto_presenter.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'background.dart';

//FOLLOWING DART CODE COPYRIGHT OF 2017 - 2018 SQUARED SOFTWARE LONDON

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => new _HomePageState();
}

class ServerStatusScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        iconTheme: new IconThemeData(color: Colors.white),
        centerTitle: true,
        backgroundColor: Colors.black,
        title: new Text(
          'API Server Status',
          textAlign: TextAlign.center,
          style: new TextStyle(
              color: Colors.white, fontSize: 27.5, fontFamily: 'Kanit'),
        ),
      ),
      body: new Center(
        child: new Column(
          children: [
            new Divider(color: Colors.white),
            new Text(
              'News Feed: ',
              textAlign: TextAlign.center,
              style: new TextStyle(
                color: Colors.black,
                fontSize: 27.5,
                fontFamily: 'Kanit',
              ),
            ),
            new Divider(),
            new Text(
              'Crypto Feed: ',
              textAlign: TextAlign.center,
              style: new TextStyle(
                color: Colors.black,
                fontSize: 27.5,
                fontFamily: 'Kanit',
              ),
            ),
            new Divider(),
            new Wrap(
              alignment: WrapAlignment.center,
              children: <Widget>[
                new Chip(
                  backgroundColor: Colors.black,
                  label: new Text(
                    '© 2017-2018 Squared Software',
                    style: new TextStyle(
                      fontSize: 15.0,
                      fontFamily: 'Poppins',
                      color: Colors.white,
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class MoreInfoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final ThemeData themeData = Theme.of(context);
    final TextStyle aboutTextStyle = themeData.textTheme.body2;
    final TextStyle linkStyle =
        themeData.textTheme.body2.copyWith(color: themeData.accentColor);
    return new Scaffold(
      appBar: new AppBar(
        iconTheme: new IconThemeData(color: Colors.white),
        centerTitle: true,
        backgroundColor: Colors.black,
        title: new Text(
          'More Info',
          textAlign: TextAlign.center,
          style: new TextStyle(
              color: Colors.white, fontSize: 27.5, fontFamily: 'Kanit'),
        ),
      ),
      body: new Center(
        child: new Column(
          children: [
            new Divider(color: Colors.white),
            new ListTile(
                title: new Text('Squared Software',
                    style: new TextStyle(
                      fontWeight: FontWeight.w500,
                      fontFamily: 'Poppins',
                    )
                  ),
                leading: new CircleAvatar(
                    radius: 30.0,
                    backgroundImage: new AssetImage(
                        'images/sqinterlock.png'
                        )
                      )
                    ),
            new Divider(),
            new Text('Where do we get our information?',
                style: new TextStyle(
                  color: Colors.black,
                  fontFamily: 'Poppins',
                  fontSize: 16.5,
                )
              ),
            new Divider(color: Colors.white),
            new Text(
              "News Feed: bit.ly/2MFpzHX",
              style: new TextStyle(
                fontFamily: 'Poppins',
                fontSize: 16.5,
              ),
            ),
            new Divider(color: Colors.white),
            new Text(
              "Crypto Feed: bit.ly/2iIdJht",
              style: new TextStyle(
                fontFamily: 'Poppins',
                fontSize: 16.5,
              ),
            ),
            new Divider(color: Colors.white),
            new Wrap(
              alignment: WrapAlignment.center,
              children: <Widget>[
                new Chip(
                  backgroundColor: Colors.black,
                  label: new Text(
                    '© 2017-2018 Squared Software',
                    style: new TextStyle(
                      fontSize: 15.0,
                      fontFamily: 'Poppins',
                      color: Colors.white,
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _HomePageState extends State<HomePage> implements CryptoListViewContract {
  CryptoListPresenter _presenter;
  List<Crypto> _currencies;
  bool _isLoading;
  final List<MaterialColor> _colors = [Colors.blue, Colors.indigo, Colors.red];

  _HomePageState() {
    _presenter = new CryptoListPresenter(this);
  }

  @override
  void onLoadTrendingComplete(Trending trending) {
    // TODO:
    articlesMap = trending.articles;

    for (Map articleMap in articlesMap) {
      articles.add(Articles.fromMap(articleMap));
    }

    if (mounted) setState(() {});
  }

  @override
  void onLoadTrendingError() {
    // TODO:
  }

  List articlesMap = [];
  List<Articles> articles = [];

  @override
  void initState() {
    super.initState();
    _isLoading = true;
    _presenter.loadCurrencies();
    _presenter.loadTrending();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text(
            "Cryp - Tick Exchange",
            style: new TextStyle(
              color: Colors.white,
              fontFamily: 'Poppins',
              fontSize: 22.5,
            ),
          ),
          iconTheme: new IconThemeData(color: Colors.white),
          backgroundColor: const Color(0xFF273A48),
          elevation: 0.0,
          centerTitle: true,
        ),
        drawer: new Drawer(
          child: new ListView(padding: EdgeInsets.zero, children: <Widget>[
            new DrawerHeader(
              child: new CircleAvatar(
                child: new Image.asset('images/ctavatar.png'),
              ),
              decoration: new BoxDecoration(
                color: Colors.black,
              ),
            ),
            new MaterialButton(
                child: new Text(
                  'Server Status',
                  textAlign: TextAlign.center,
                  style: new TextStyle(fontSize: 27.5, fontFamily: 'Kanit'),
                ),
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                        builder: (context) => ServerStatusScreen()),
                  );
                }),
            new Divider(),
            new MaterialButton(
                child: new Text(
                  'More Info',
                  textAlign: TextAlign.center,
                  style: new TextStyle(fontSize: 27.5, fontFamily: 'Kanit'),
                ),
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => MoreInfoScreen()),
                  );
                }),
            new Divider(),
            new Wrap(
              alignment: WrapAlignment.center,
              children: <Widget>[
                new Chip(
                  backgroundColor: Colors.black,
                  label: new Text(
                    'v0.0.1',
                    style: new TextStyle(
                      fontSize: 15.0,
                      fontFamily: 'Poppins',
                      color: Colors.white,
                    ),
                  ),
                ),
              ],
            ),
          ]),
        ),
        body: _isLoading
            ? new Center(child: new CupertinoActivityIndicator(radius: 15.0))
            : _allWidget());
  }


  Widget _allWidget() {
    final _width = MediaQuery.of(context).size.width;
    final _height = MediaQuery.of(context).size.height;
//CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED
    final headerList = new ListView.builder(
      itemBuilder: (context, index) {
        EdgeInsets padding = index == 0
            ? const EdgeInsets.only(
                left: 20.0, right: 10.0, top: 4.0, bottom: 30.0)
            : const EdgeInsets.only(
                left: 10.0, right: 10.0, top: 4.0, bottom: 30.0);
        return new Padding(
          padding: padding,
          child: new InkWell(
            onTap: () {
              print('@url');
            },
            child: new Container(
              decoration: new BoxDecoration(
                borderRadius: new BorderRadius.circular(10.0),
                color: const Color(0xFF273A48),
                boxShadow: [
                  new BoxShadow(
                    color: Colors.black.withAlpha(70),
                    offset: const Offset(3.0, 10.0),
                      blurRadius: 15.0)
                ],
                image: new DecorationImage(
                  image: new NetworkImage(articles[index].urlToImage),
                  fit: BoxFit.fitHeight,
                ),
              ),
              height: 200.0,
              width: 275.0,
              child: new Stack(
                children: <Widget>[
                  new Align(
                    alignment: Alignment.bottomCenter,
                    child: new Container(
                      padding: new EdgeInsets.only(left: 10.0),
                        decoration: new BoxDecoration(
                          color: const Color(0xFF273A48),
                          borderRadius: new BorderRadius.only(
                              bottomLeft: new Radius.circular(10.0),
                              bottomRight: new Radius.circular(10.0)),
                        ),
                        height: 50.0,
                        child: new Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: <Widget>[
                            new Expanded(child: new Text(
                              articles[index].title,
                              overflow: TextOverflow.ellipsis,
                              maxLines: 2,
                              style: new TextStyle(
                                color: Colors.white,
                                fontFamily: 'Poppins',
                              ),
                            ),
                          ),
                        ],
                      )
                    ),
                  )
                ],
              ),
            ),
          ),
        );
      },
      scrollDirection: Axis.horizontal,
      itemCount: articles.length,
    );

    final body = new Scaffold(
      backgroundColor: Colors.transparent,
      body: new Container(
        child: new Stack(
          children: <Widget>[
            new Padding(
              padding: new EdgeInsets.only(top: 10.0),
              child: new Column(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  new Align(
                    alignment: Alignment.centerLeft,
                    child: new Padding(
                        padding: new EdgeInsets.only(
                          left: 10.0,
                        ),
                        child: new Text(
                          "Trending News",
                          style: new TextStyle(
                            letterSpacing: 0.8,
                            fontFamily: 'Kanit',
                            fontSize: 17.5,
                            color: Colors.white,
                          ),
                        )),
                  ),
                  new Container(
                      height: 300.0, width: _width, child: headerList),
                  new Expanded(child: ListView.builder(
                      itemBuilder: (BuildContext context, int index) {
                    final int i = index;
                    final Crypto currency = _currencies[i];
                    final MaterialColor color = _colors[i % _colors.length];
                    return new ListTile(
                      title: new Column(
                        children: <Widget>[
                          new Row(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: <Widget>[
                              new Container(
                                height: 72.0,
                                width: 72.0,
                                decoration: new BoxDecoration(
                                    color: Colors.white,
                                    boxShadow: [
                                      new BoxShadow(
                                          color: Colors.black.withAlpha(80),
                                          offset: const Offset(2.0, 2.0),
                                          blurRadius: 15.0)
                                    ],
                                    borderRadius: new BorderRadius.all(
                                        new Radius.circular(35.0)),
                                    image: new DecorationImage(
                                      image: new ExactAssetImage(
                                        "cryptoiconsBlack/" +
                                            currency.symbol.toLowerCase() +
                                            "@2x.png",
                                      ),
                                      fit: BoxFit.cover,
                                    )),
                              ),
                              new SizedBox(
                                width: 8.0,
                              ),
                              new Expanded(
                                  child: new Column(
                                mainAxisAlignment: MainAxisAlignment.start,
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  new Text(
                                    currency.name,
                                    style: new TextStyle(
                                        fontSize: 15.0,
                                        fontFamily: 'Poppins',
                                        color: Colors.black87,
                                        fontWeight: FontWeight.bold),
                                  ),
                                  _getSubtitleText(currency.price_usd,
                                      currency.percent_change_1h),
                                ],
                              )),
                            ],
                          ),
                          new Divider(),
                        ],
                      ),
                    );
                  }))
                ],
              ),
            ),
          ],
        ),
      ),
    );

    return new Container(
      decoration: new BoxDecoration(
        color: const Color(0xFF273A48),
      ),
      child: new Stack(
        children: <Widget>[
          new CustomPaint(
            size: new Size(_width, _height),
            painter: new Background(),
          ),
          body,
        ],
      ),
    );
  }

// CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED

  Widget _getSubtitleText(String priceUSD, String percentageChange) {
    TextSpan priceTextWidget = new TextSpan(
        text: "$$priceUSD\n",
        style: new TextStyle(
          color: Colors.black,
          fontSize: 14.0,
        ));
    String percentageChangeText = "1 hour: $percentageChange%";
    TextSpan percentageChangeTextWidget;

    if (double.parse(percentageChange) > 0) {
      percentageChangeTextWidget = new TextSpan(
          text: percentageChangeText,
          style: new TextStyle(
            color: Colors.green,
            fontFamily: 'PoppinsMediumItalic',
          ));
    } else {
      percentageChangeTextWidget = new TextSpan(
          text: percentageChangeText,
          style: new TextStyle(
            color: Colors.red,
            fontFamily: 'PoppinsMediumItalic',
          ));
    }

    return new RichText(
        text: new TextSpan(
            children: [priceTextWidget, percentageChangeTextWidget]));
  }

  //Works with cryptoListViewContract implimentation in _MyHomePageState
  @override
  void onLoadCryptoComplete(List<Crypto> items) {
    // TODO: implement onLoadCryptoComplete

    setState(() {
      _currencies = items;
      _isLoading = false;
    });
  }

  @override
  void onLoadCryptoError() {
    // TODO: implement onLoadCryptoError
  }
}

感谢杰克的帮助

根据您想要的最终体验,可能有多种实现方法。一个简单的解决方案是 创建 activeSearch 切换 'search app bar' 和 'normal app bar'

的状态

这是正常的应用栏:

return AppBar(
  title: Text("My App"),
  actions: <Widget>[
    IconButton(
      icon: Icon(Icons.search),
      onPressed: () => setState(() => activeSearch = true),
    ),
  ],
);

这是搜索应用栏:

return AppBar(
  leading: Icon(Icons.search),
  title: TextField(
    decoration: InputDecoration(
      hintText: "here's a hint",
    ),
  ),
  actions: <Widget>[
    IconButton(
      icon: Icon(Icons.close),
      onPressed: () => setState(() => activeSearch = false),
    )
  ],
);

注意:如果您不想在搜索处于活动状态时使用前导图标,您可能需要禁用 drawerback button 图标的默认行为:

automaticallyImplyLeading: false

完整示例:

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool activeSearch;

  @override
  void initState() {
    super.initState();
    activeSearch = false;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _appBar(),
      drawer: _drawer(),
    );
  }

  PreferredSizeWidget _appBar() {
    if (activeSearch) {
      return AppBar(
        leading: Icon(Icons.search),
        title: TextField(
          decoration: InputDecoration(
            hintText: "here's a hint",
          ),
        ),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.close),
            onPressed: () => setState(() => activeSearch = false),
          )
        ],
      );
    } else {
      return AppBar(
        title: Text("My App"),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () => setState(() => activeSearch = true),
          ),
        ],
      );
    }
  }

  Widget _drawer() {
    return Container();
  }
}

更新:这是处理结果的提示

return AppBar(
  ...
  title: TextField(
    onChanged: _search,
  ),
);

以及 _search 可能 的样子:

  List<MyResultObject> _results;

  void _search(String queryString) {
    // do some searching and sorting
    // then call setState() with the results
    // and then in your ListView you can read from results
    // (handle empty, default case as well in view)
    setState(() {
      _results = ...
    });
  }

  List<Widget> _resultWidgets() {
    if (_results.isEmpty) return _defaultWidgets();
    _results.map((r) => _buildRowWidget(s)).toList();
  }

您能否在 this 答案中引用一个简单的搜索视图。在该示例中,随着用户键入,列表将被过滤。