Flutter - 实现 listView 搜索功能

Posted

技术标签:

【中文标题】Flutter - 实现 listView 搜索功能【英文标题】:Flutter - Implementing a listView search feature 【发布时间】:2019-02-13 15:48:29 【问题描述】:

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

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

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

这就是我的“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
  

谢谢你的帮助,杰克

【问题讨论】:

你考虑过AppBar.actions吗?如果是这样,期望的效果是什么?也就是说,你想要一个弹出对话框来搜索吗?拥有一个 TextField 而不是一个 Icon 会很酷,您只需在隐藏 AppBar 的其他部分的同时扩展它。 是的,我将在此之后编辑问题以反映此评论,但无论如何,当您单击搜索 iconButton 时,搜索字段可以替换 appBar? 中的标题。对于用户来说,这是针对加密 listView 的显而易见的,因为我将在搜索视图中添加一个提示来标识这一点。谢谢 使用这个插件并随心所欲地使用它:- ***.com/a/61727414/10563627 你可以使用 Flutter 库来实现与 Youtube/Instagram 相同的设计:github.com/pkmangukiya/flutter_search_view_pk 【参考方案1】:

根据您想要的最终体验,可能有很多方法可以实现这一点。一个简单的解决方案是创建activeSearch 状态来切换“搜索应用栏”和“普通应用栏”

这是正常的应用栏:

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();
  

【讨论】:

这看起来很棒,现在就实现它 您是否可以提供或提示我实际对项目进行排序所需的代码?搜索栏非常好,感谢您到目前为止所做的一切。 @Jake,我在最后添加了快速更新,可能会有所帮助 太棒了,感谢您的帮助。我会看一下@Dinesh Balasubramanian 提供的问题并尽快参考。 嘿@Ashton,刚刚注意到当我在搜索字段中键入一个值时弹出屏幕上的ios键盘时,我得到了严重的像素溢出。对此我能做些什么吗?谢谢【参考方案2】:

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

【讨论】:

我会先实现来自 Ashton 的问题,然后再添加。感谢您提供更多信息:)

以上是关于Flutter - 实现 listView 搜索功能的主要内容,如果未能解决你的问题,请参考以下文章

Flutter:无法将 ListView 添加到示例应用程序

Flutter 错误:'UnmodifiableUint8ListView' 受到限制,无法扩展或实现

Flutter:在 http.get 请求之后构建 ListView

在 Flutter 应用程序的 ListView.builder 中添加滚动

善用 FetchMore GQL 实现无限滚动分页 ListView with Flutter

flutter 实现不可滚动的ListView构建器