目录
  • 效果图
  • 顶部搜索栏
    • SearchBar 实现细节
      • 左边搜索框实现
      • 右边取消按钮实现
  • 内容的检索
    • 内容的传递
    • 内容的检索
      • 搜索列表实现
        • 总结

          效果图

          如上图所示,我们用 Flutter 来仿写搜索页面,这里聊天首页点击搜索栏会跳转到搜索页,搜索页面包含顶部搜索框跟底部 ListView,在搜索框内我们输入搜索词会检索聊天列表模型中 name 属性中包含搜索词的模型,并在底部列表中展示,且搜索词高亮显示。下面我们分别来介绍下这些功能的实现。

          顶部搜索栏

          class SearchBar extends StatefulWidget {
            final ValueChanged<String>? onChanged;
            const SearchBar({this.onChanged});
          
            @override
            _SearchBarState createState() => _SearchBarState();
          }
          
          class _SearchBarState extends State<SearchBar> {
            final TextEditingController _editingController = TextEditingController();
            bool _showClose = false;
            void _onChange(text) {
              if (null != widget.onChanged) {
                widget.onChanged!(text);
              }
              setState(() {
                _showClose = ((text as String).length > 0);
              });
            }
          
            @override
            Widget build(BuildContext context) {
              return Container(
                height: 84,
                color: CahtThemColor,
                child: Column(
                  children: [
                    SizedBox(height: 40,), //上半部分留空
                    Container(
                      height: 44,
                      child: Row(
                        children: [
                          Container(
                            width: screenWidth(context) - 50,
                            height: 34,
                            margin: EdgeInsets.only(left: 5, right: 10),
                            padding: EdgeInsets.only(left: 5, right: 5),
                            decoration: BoxDecoration(
                              color: Colors.white,
                              borderRadius: BorderRadius.circular(6.0),
                            ),
                            child: Row(
                              children: [
                                Image(image: AssetImage('images/放大镜b.png'), width: 20, color: Colors.grey,), //放大镜
                                Expanded(child: TextField(
                                  controller: _editingController,
                                  onChanged: _onChange,
                                  autofocus: true,
                                  cursorColor: Colors.green,
                                  style: TextStyle(
                                    fontSize: 16.0,
                                    color: Colors.black87,
                                    fontWeight: FontWeight.w400,
                                  ),
                                  decoration: InputDecoration(
                                    contentPadding: EdgeInsets.only(left: 5, right: 5, bottom: 12,),
                                    border: InputBorder.none,
                                    hintText: '搜索',
                                    hintStyle: TextStyle(
                                      fontSize: 16.0,
                                      color: Colors.grey,
                                      fontWeight: FontWeight.w400,
                                    ),
                                  ),
                                ),),
                                if (_showClose) GestureDetector(
                                  onTap: () {
                                    //清空搜索框
                                    _editingController.clear();
                                    setState(() {
                                      _onChange('');
                                    });
                                  },
                                  child: Icon(
                                    Icons.cancel,
                                    color: Colors.grey,
                                    size: 20,
                                  ),
                                ),
                              ],
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            ),
                          ), //左边白色背景
                          GestureDetector (
                            onTap: () {
                              Navigator.pop(context);
                            },
                            child: Text('取消'),
                          ), //右边取消按钮
                        ],
                      ),
                    ), //搜索条部分
                  ],
                ),
              );
            }
          }

          针对搜索页的整体布局我们可以分为顶部的搜索栏跟底部的搜索列表两部分,针对搜索栏我们单独定义了一个 SearchBar 的类来实现,底部搜索列表我们用 Expanded 部件进行了包装。

          SearchBar 实现细节

          针对 SearchBar 我们这里整体高度设置为了 84,这里最好高度定义为一个变量,根据机型来设置。针对搜索栏我们分为上下两部分,顶部留白及内容部分,顶部留白用 SizedBox 部件实现,底部内容用 Row 部件来实现,分为左边搜索框及右边取消按钮。

          左边搜索框实现

          搜索框整体部分我们用 Container 部件来实现,可以设置宽、高及圆角属性。child 属性我们也是用 Row 部件来实现,分为左边搜索图标、中间 TextField 及右边 关闭按钮。

          TextField 实现细节

          TextField 部件有两个比较重要的属性,onChangedcontrolleronChanged 我们传入了一个外部方法 _onChange,可以监听输入框文字的变化,当有文字时展示关闭按钮,没有文字时隐藏关闭按钮。controller 我们传入了一个外部定义的属性 _editingController,可以用来控制 TextField 的编辑。

          关闭按钮的实现

          关闭按钮我们根据 _showClose 来判断是否展示,且通过 GestureDetector 部件进行包装,点击的时候清空输入框及调用 _onChange 方法,参数传入空字符串,_onChange 方法中字符串为空的时候就会隐藏关闭按钮。

          右边取消按钮实现

          取消按钮点击的时候就是返回到上一个页面。

          内容的检索

          我们监听到文字输入框的变化后,就需要进行内容的检索,并且在底部列表中进行展示。

          内容的传递

          class SearchCell extends StatelessWidget {
            final List<ChatModel>? datas;
            const SearchCell({this.datas});
          }
          class SearchPage extends StatefulWidget {
            final List<ChatModel>? datas;
            const SearchPage({this.datas});
          }

          这里我们是基于聊天页面列表数据模型进行检索,找到名称中包含搜索词的模型进行展示。所以我们在 SearchCell 中定义了 datas 属性,并且在 SearchPage 中也定义了 datas 属性,分别在初始化的时候进行传递,这样我们在搜索页面就可以拿到了聊天列表的模型数据。

          内容的检索

          //满足查找条件的数据数组
            List<ChatModel> _models = [];
            //正在搜索的内容
            String _searchStr = '';
            // 搜索数据查找
            void _searchData(String text) {
              //每次搜索前先清空
              _models.clear();
              _searchStr = text;
              if (text.length < 1) {
                setState(() {});
                return;
              }
              for (int i = 0; i < datas.length; i++) {
                String name = widget.datas?[i].name as String;
                if(name.contains(text)) {
                  _models.add(widget.datas?[i] as ChatModel);
                }
              }
              setState(() {});
            }

          我们把检索逻辑都抽取到了 _searchData 方法中,输入内容变化的时候调用 _searchData 方法。这里我们定义了一个 _models 数组,专门存放检索数据,我们遍历 datas,把检索到的模型添加到 _models 中。

          搜索列表实现

          TextStyle _normalStyle = TextStyle(
              fontSize: 16,
              color: Colors.black,
            );
            TextStyle _hightlightedStyle = TextStyle(
              fontSize: 16,
              color: Colors.green,
            );
          
            Widget _searchTitle(String name) {
              List<TextSpan> textSpans = [];
          
              List<String> searchStrs = name.split(_searchStr);
              for (int i = 0; i < searchStrs.length; i++) {
                String str = searchStrs[i];
                if (str == '' && i < searchStrs.length - 1) {
                  textSpans.add(TextSpan(text: _searchStr, style: _hightlightedStyle));
                } else {
                  textSpans.add(TextSpan(text: str, style: _normalStyle));
                  if (i < searchStrs.length - 1) {
                    textSpans.add(TextSpan(text: _searchStr, style: _hightlightedStyle));
                  }
                }
              }
              return RichText(text: TextSpan(children: textSpans));
            }

          搜索列表的 cell 就是复用聊天列表的 cell,但是需要变化的是搜索内容的高亮显示,所以 ListTile 部件的 title 属性我们用 RichText 来实现,实现内容这里抽取到了 _searchTitle 方法中。name.split(_searchStr) 会返回一个根据搜索词 _searchStr 进行分割的数组,但是当字符串的开头或者结尾有跟 _searchStr 相同的字符的时候在 searchStrs 数组中的元素就是空字符串,所以我们就需要进行特别判 str == ''。我们循环遍历 searchStrs 数组来创建 TextSpan 部件,并且根据内容是否跟搜索内容一样来分别指定样式 _normalStyle 与 _hightlightedStyle。

          总结