Flutter —— 搜索框

2,960 阅读2分钟

这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战

在聊天界面添加一个搜索框。那么就在ListView里面添加一个cell,那么就需要itemCount里面加1。

  itemCount: _datas.length + 1,

创建一个chat package,然后将chat_page拖进来并且重新创建一个search_cell文件。

在这里插入图片描述

将ListView里面的itemBuilder的代码抽取成一个方法,然后在里面判断如果index == 0 则返回 SearchCell。这里后面需要index--,否则index就会从1开始。

在这里插入图片描述

接下来写search cell 的界面,先赋予一个高度和颜色,来看是否能显示。

 return Container(
      height: 45,
      width:  200,
      color: Colors.red,
    );

运行后看到显示了出来。

在这里插入图片描述

搜索框需要响应点击时间,所以这里需要用GestureDetector包一下,然后颜色改为weChatThemColor,里面用Stack来写。

return GestureDetector(
      child: Container(
      height: 45,
      width:  200,
      color: weChatThemColor,
       padding: EdgeInsets.all(5),
        child: Stack(
          children: [

          ],
        ),
    ),

    );

在stack里面先添加一个白底

 Container(
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(6.0),
              ),
            ),//白底

然后添加Row里面放图片和文字,为了让其居中设置row的mainAxisAlignment为MainAxisAlignment.center,而让row居中则将stack的alignment改为Alignment.center。

 child: Stack(
          alignment: Alignment.center,
          children: [
            Container(
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(6.0),
              ),
            ),//白底
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Image(image: AssetImage('images/放大镜b.png'),width: 15,color: Colors.grey,),
                Text('   搜索',style: TextStyle(fontSize: 15,color: Colors.grey),),

            ],),

          ],
        ),

这样搜索框页面就完成了

在这里插入图片描述

接下来点进去需要到一个新的界面,那么就添加onTap,然后创建一个文件来写新页面SearchPage。

import 'package:flutter/material.dart';

class SearchPage extends StatefulWidget {
  const SearchPage({Key? key}) : super(key: key);

  @override
  _SearchPageState createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  @override
 Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          SearchBar(),
          Expanded(
            flex: 1,
            child: ListView.builder(
              itemBuilder: itemBuilder,
              itemCount: 3,
            ),
          ),
        ],
      ),
    );
  }

  Widget itemBuilder(BuildContext context, int index) {
    return Container(
      child:Text("Column$index"),
    );
  }
}

class SearchBar extends StatefulWidget {
  const SearchBar({Key? key}) : super(key: key);

  @override
  _SearchBarState createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 84,
      color: weChatThemColor,
    );
  }
}

然后在SearchCell的GestureDetector里面添加页面push。

   onTap: (){
        Navigator.of(context).push(MaterialPageRoute(
            builder: (BuildContext context) =>
                SearchPage()));
      },

运行后得到下面的界面:

在这里插入图片描述

发现这里有间距,那么就使用removePadding将ListView头部间距去掉。

    child: MediaQuery.removePadding(
              removeTop: true,
              context: context,
              child: ListView.builder(
                itemBuilder: itemBuilder,
                itemCount: 3,
              ),
            ),

然后开始编写SearchBar。

return Container(
      height: 84,
      color: weChatThemColor,
      child: Column(
        children: [
          SizedBox(height: 40,),
          Container(
            height: 44,
            color:Colors.red,
            child: Row(
              children: [
                Container(
                  width: screenWidth(context) - 40,
                  height: 34,
                  color: Colors.yellow,
                ),//圆角背景
                Text('取消'),//取消按钮
              ],
            ),
          ),
        ],
      ),
    );

运行后看到:

在这里插入图片描述

然后在添加圆角,间距以及按钮等。

    return Container(
      height: 84,
      color: weChatThemColor,
      child: Column(
        children: [
          SizedBox(
            height: 40,
          ),
          Container(
            height: 44,
            color: Colors.red,
            child: Row(
              children: [
                Container(
                  width: screenWidth(context) - 50,
                  height: 34,
                  margin: EdgeInsets.only(left: 5, right: 5),
                  padding: EdgeInsets.only(left: 5, right: 5),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(6.0),
                  ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Image(
                        image: AssetImage('images/放大镜b.png'),
                        width: 20,
                        color: Colors.grey,
                      ), //放大镜
                      Icon(Icons.cancel),
                    ],
                  ),
                ), //圆角背景

                Text('取消'), //取消按钮
              ],
            ),
          ),
        ],
      ),
    );

然后还需要添加TextField。

  return Container(
      height: 84,
      color: weChatThemColor,
      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: 5),
                  padding: EdgeInsets.only(left: 5, right: 5),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(6.0),
                  ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: const [
                      Image(
                        image: AssetImage('images/放大镜b.png'),
                        width: 20,
                        color: Colors.grey,
                      ), //放大镜
                      Expanded(
                        child: TextField(
                          cursorColor: Colors.green,
                          autofocus: true,
                          style: TextStyle(
                            fontSize: 18.0,
                            color: Colors.black,
                            fontWeight: FontWeight.w300,

                          ),
                          decoration: InputDecoration(
                            contentPadding: EdgeInsets.only(left: 5,bottom: 10),
                            border:InputBorder.none,
                            hintText:'搜索',
                          ),
                        ),
                      ),
                      Icon(Icons.cancel,size: 20,color: Colors.grey,),
                    ],
                  ),
                ), //圆角背景

                Text('取消'), //取消按钮
              ],
            ),
          ),
        ],
      ),
    );

之前用Expanded包着ListView是因为ListView没有大小,当我们设置了 shrinkWrap: true之后,那么就会根据ListView的内容大小来展示,就不需要Expanded了。我们这里还是用之前的写法,让ListView占满屏幕下面部分。

   return Scaffold(
      body: Column(
        children: [
          SearchBar(),
          ListView.builder(
            shrinkWrap: true,
            itemBuilder: itemBuilder,
            itemCount: 3,
          ),
        ],
      ),
    );

接下来处理点击和逻辑事件

这里先为取消添加一个pop的点击事件。

  GestureDetector(
                  child: Text('取消'),
                  onTap: () {
                    Navigator.pop(context);
                  },
                ),

接下来需要监听搜索框,当没有任何东西的时候,那么就不显示cancel icon。创建一个TextEditingController变量。

  final TextEditingController _textEditingController = TextEditingController();

为TextField添加Controller

  controller: _textEditingController,

然后监听TextField的onChanged

   onChanged: _onChanged,
  void _onChanged(value) {}

然后创建_showClear来判断是否显示TextField后面的取消按钮,默认为false。

 bool _showClear = false; 

在_onChanged方法里面进行字符串长度的判断以及_showClear的赋值。

  void _onChanged(String text) {
    if (text.length > 0) {
      setState(() {
        _showClear = true;
      });
  
    } else {
      setState(() {
        _showClear = false;
      });
    }
  }

根据_showClear的值觉得是否显示Icon。

   if (_showClear) Icon(
                              Icons.cancel,
                              size: 20,
                              color: Colors.grey,
                            )
 

为 cancel Icon添加点击手势清空输入内容。

   if (_showClear) GestureDetector(
                        child: Icon(
                          Icons.cancel,
                          size: 20,
                          color: Colors.grey,
                        ),
                        onTap: () {
                          _textEditingController.clear();
                          setState(() {
                            _onChanged("");
                          });
                        },
                      )

这样页面部分就完成了。这里有个问题,ChatPage点击进去SearchPage的时候那么SearchPage一定要有数据,那么SearchBar 是否有数据呢?这里有两种情况,一种是有,将数据传给SearchBar,搜索完之后将结果返回给SearchPage,第二种是没有,SearchBar直接通过回调将输入内容给SearchPage。 先在SearchCell添加

  final List<Chat>? datas;
  const SearchCell({ this.datas});

然后在ChatPage为其赋值,这样数据就给了SearchCell。

   if (index == 0) {
      return SearchCell(datas: _datas,);
    }

然后再传给SearchPage。在SearchPage添加:

  final List<Chat>? datas;
  const SearchPage({ this.datas});

然后在SearchCell进来的地方将data传进来

SearchPage(datas: datas,)

在SearchBar里面添加一个回调:

  const SearchBar({Key? key, this.onChanged}) : super(key: key);
  final ValueChanged<String>? onChanged;

然后在_onChanged里面调用这个回调。这里if else 判断太长了,可以直接使用 _showClear = text.length > 0进行设值。

  void _onChanged(String text) {
    if (widget.onChanged != null) {
      widget.onChanged!(text);
    }
    setState(() {
      _showClear = text.length > 0;
    });
  
  }

这个时候SearchPage就可以传回调了

 SearchBar(onChanged: ( String text){},),

创建一个 _searchData方法来处理回调传过来的文字。

  SearchBar(onChanged: (String text){
            _searchData(text);
          },),
  void _searchData(String text) {
}

接下来创建一个数组来装符合条件的数据

List<Chat> _models = [];

_searchData里面循环检索然后添加符合条件的数据到_models。

  void _searchData(String text) {
    _models.clear(); // 每次搜索先情况
    if (text.length > 0 ) {
      if (widget.datas != null){   // 循环检索
        for (int i = 0; i < widget.datas!.length;i++ ) {
         String? name =  widget.datas![i].name;
         if ((name ?? "").contains(text)) {
           _models.add(widget.datas![i]);
         }
        }
      }
    }
    setState(() {
      
    });
  }

接下来根据_models显示ListView的内容,将itemCount改为_models.length。

  itemCount: _models.length,

在itemBuilder里面返回

  Widget itemBuilder(BuildContext context, int index) {
    return Container(
      color: Colors.red,
      child: Text("${_models[index].name}"),
    );
  }

这样搜索就可以显示符合条件的Model了。

在这里插入图片描述

接下来需要来编写Cell的界面,这里可以直接把之前聊天界面的样式复制过来。

ListTile(

      title: Text(_models[index].name ?? ""),
      subtitle: Container(
        alignment: Alignment.bottomCenter,
        padding: EdgeInsets.only(right: 10),
        height: 25,
        child: Text(
          _models[index].message ?? "",
          overflow: TextOverflow.ellipsis,
        ),
      ),
      leading: ClipRRect(
        //剪裁为圆角矩形
        borderRadius: BorderRadius.circular(5.0),
        child: Image(image: NetworkImage(_models[index].imageUrl ?? "")),
      ),
    );

运行后:

在这里插入图片描述

这里需要对名字做高亮处理,那么就把title部分提取出来做处理。

    title: _title(_models[index].name ?? ""),

  Text _title(String name) {
    return Text(name);
  }

我们要知道搜索的名字是什么,所以这里声明一个变量

 String _searchStr = "";

然后在_searchData里面赋值。

_searchStr = text

然后在声明2个TextStyle

  TextStyle _normalStyle = TextStyle(fontSize: 16,color: Colors.black);
  TextStyle _highLightedStyle = TextStyle(fontSize: 16,color: Colors.green);

然后再_title方法里面处理。

idget _title(String name) {
    List<TextSpan> spans = [];
    List<String> strs = name.split(_searchStr);
    for (int i = 0; i < strs.length; i ++) {
      String str = strs[i];
      print(strs);
      if (str == "" && i < strs.length - 1) {
        print('here1');
        spans.add(TextSpan(text: _searchStr,style:_highLightedStyle));
      } else {
        print('here2');
        spans.add(TextSpan(text: str,style:_normalStyle));
        if (i < strs.length - 1) {
          print('here3');
          spans.add(TextSpan(text: _searchStr,style:_highLightedStyle));
        }
      }
    }

这样搜索高亮就完成了。

在这里插入图片描述