本篇最终效果

在首页新增一个搜索条,点击的时候跳转到新的页面,输入能够实时检索。
image.png

分析思路

这里的searchBar是跟着ListView可以上下一起移动的,所以把这个搜索框放入ListView里, 那么itemCount: _datas.length + 1,准备改造下itemBuilder ,所以先把这里抽取出来。

方法抽取的快捷键command+option+M,弹出对话框输入方法名称Refactor就OK了
image.png
在这里index=0的时候加入一个搜索框,为了保证后面的数据不紊乱,需要index--

  1. Widget _itemBuilderForRow(BuildContext context, int index) {
  2. if (index == 0) {
  3. return Container(color: Colors.red, height: 44);
  4. }
  5. index--;
  6. return ListTile(
  7. title: Text(_datas[index].name!),
  8. subtitle: Container(
  9. alignment: Alignment.bottomCenter,
  10. padding: EdgeInsets.only(right: 10),
  11. height: 20,
  12. child: Text(
  13. _datas[index].message!,
  14. overflow: TextOverflow.ellipsis,
  15. ),
  16. ),
  17. leading: CircleAvatar(
  18. backgroundImage: NetworkImage(_datas[index].imgUrl!),
  19. ),
  20. );
  21. }

image.png

SearchCell

能触发点击的可以使用GestureDetector,布局方面使用Stack + row混合搭配使用就能达到

  1. import 'package:flutter/material.dart';
  2. class SearchCell extends StatelessWidget {
  3. @override
  4. Widget build(BuildContext context) {
  5. return GestureDetector(
  6. child: Container(
  7. color: Color.fromRGBO(238, 238, 238, 1),
  8. height: 44,
  9. padding: EdgeInsets.all(5),
  10. child: Stack(
  11. children: [
  12. Container(
  13. decoration: BoxDecoration(
  14. color: Colors.white, borderRadius: BorderRadius.circular(5)),
  15. ),
  16. Container(
  17. height: 44,
  18. child: Row(
  19. mainAxisAlignment: MainAxisAlignment.center,
  20. children: [
  21. Image.asset(
  22. 'images/放大镜b.png',
  23. width: 15,
  24. color: Colors.grey,
  25. ),
  26. Text(
  27. ' 搜索 ',
  28. style: TextStyle(fontSize: 15, color: Colors.grey),
  29. )
  30. ],
  31. ),
  32. )
  33. ],
  34. ),
  35. ),
  36. );
  37. }
  38. }

SearchPage

首先这个搜索页面是有状态的,同时是一个没有AppBar的一个Column布局的一个视图。上面是一个SearchBar,下面是一个ListView且支持富文本显示。
所以在SearchCell点击的时候,跳转新页面

  1. Navigator.of(context).push(
  2. MaterialPageRoute(builder: (BuildContext context) => SearchPage()));

SearchPage页面布局如下,我们可以给Scaffold不设置appBar属性

  1. class _SearchPageState extends State<SearchPage> {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. body: Column(
  6. children: [
  7. SearchBar(),
  8. Expanded(
  9. child: ListView.builder(
  10. itemCount: 10,
  11. itemBuilder: (BuildContext context, int index) {
  12. return Text('123$index');
  13. })
  14. )
  15. ],
  16. ),
  17. );
  18. }
  19. }

此时运行效果如下
image.png
我们发现ListView上面有一点边距,这样写来去掉吧:

  1. Expanded(
  2. child: MediaQuery.removePadding(
  3. context: context,
  4. removeTop: true,
  5. child: ListView.builder(
  6. itemCount: 10,
  7. itemBuilder: (BuildContext context, int index) {
  8. return Text('123$index');
  9. })))

image.png

SearchBar

分析:这里的SearchBar整体是一个Column结构,上面是一个占位的SizeBox,下面是一个Container,Container内是一个Row布局的一个圆角装饰器和右边的一个Text

  1. class _SearchBarState extends State<SearchBar> {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Container(
  5. height: 84,
  6. color: Colors.red,
  7. child: Column(
  8. children: [
  9. SizedBox(height: 40),
  10. Container(
  11. height: 44,
  12. color: Colors.grey,
  13. child: Row(
  14. children: [
  15. Container(
  16. margin: EdgeInsets.only(left: 6, right: 6),
  17. width: screenWidth(context) - 45,
  18. height: 34,
  19. decoration: BoxDecoration(
  20. borderRadius: BorderRadius.circular(6),
  21. color: Colors.white),
  22. ),
  23. Text('取消')
  24. ],
  25. ),
  26. )
  27. ],
  28. ),
  29. );
  30. }
  31. }

运行之后的效果:
image.png
接着上面,Container内继续Row布局,左边是一个搜索的图片,中间是一个TextField,右边是一个clear的按钮

  1. Row(
  2. children: [
  3. Image.asset('images/放大镜b.png',
  4. width: 20, color: Colors.grey),
  5. Expanded(
  6. child: TextField()),
  7. Icon(
  8. Icons.clear,
  9. color: Colors.grey,
  10. size: 20,
  11. )
  12. ],
  13. ),

image.png
可以明显的发现几个问题

  • 左右间距太近
  • 光标的颜色不对
  • TextField边框有颜色
  • 文本输入的位置偏下,没有居中
  • 没有默认的占位文字

针对以上的问题,补充以下细节:

  1. padding: EdgeInsets.only(left: 6, right: 6),
  2. TextField(
  3. cursorColor: Colors.green,
  4. style: TextStyle(
  5. fontWeight: FontWeight.w300,
  6. fontSize: 16,
  7. ),
  8. decoration: InputDecoration(
  9. hintText: '搜索',
  10. border: InputBorder.none,
  11. contentPadding: EdgeInsets.only(bottom: 12, left: 6)
  12. )
  13. )

image.png
继续补充完细节:当点击取消的时候,返回上一级页面,以及当TextView有字时,clear按钮展示,反之则不展示。可以通过监听TextField的onChange的回调,这里其实跟iOS有异曲同工之妙
typedef ValueChanged<T> = void Function(T value);,新增一个变量_showClear来记录是否展示。在布局的时候,如果_showClear为真就展示,否则给一个空白的Container()

  1. void _onChanged(String value) {
  2. setState(() {
  3. _showClear = value.length > 0
  4. });
  5. }

接着,当我们点击clear按钮的时候,需要清空当前的文本输入框,所以需要给clear按钮添加一个点击事件。

  1. if (_showClear)
  2. GestureDetector(
  3. onTap: () {
  4. _controller.clear();
  5. _onChanged('');
  6. },
  7. child: Icon(
  8. Icons.clear,
  9. color: Colors.grey,
  10. size: 20,
  11. ),
  12. )

好了,到了现在已经实现了输入文本框的时候,清除按钮展示反之则隐藏。并且点击清除按钮的时候,文本框会同步清空。

数据处理

值传递:ChatPage.datas ==> SearchCell.datas ==>SearchPage.datas
回调:在初始化SearchBar的时候,定义一个回调,有点类似于闭包。

  1. final ValueChanged<String>? onChanged;
  2. const SearchBar({this.onChanged});

然后在TextField的onChanged的时候,调用这个函数

  1. if (widget.onChanged != null) {
  2. widget.onChanged!(value);
  3. }

最后在SearchPage使用也就是初始化SearchBar的地方回调

  1. SearchBar(onChanged: (value) {
  2. print('搜索$value');
  3. }),

这样就能实时监听SearchBar的文本输入,同时来检索SearchPage.datas中的数据。检索的方法还是比较简单的,值得注意的是在每次检索之后调用setState来更新数据。

  1. List<ChatModel> _models = [];
  2. void _searchData(String value) {
  3. _models.clear();
  4. if (value.length > 0 && widget.data != null) {
  5. for (int i = 0; i < widget.data!.length; i++) {
  6. String name = widget.data![i].name ?? '';
  7. print(name);
  8. if (name.contains(value)) _models.add(widget.data![i]);
  9. }
  10. }
  11. setState(() {});
  12. }

此时效果现在如下:
image.png
还有一点没有完善就是输入的文本,在对应的cell里面需要高亮显示,我们继续来实现吧!
针对这里的Text,我们可以定义一个专门的方法来自定义返回。这里我采用的是split方法。

  1. // 正常情况下的文本样式
  2. TextStyle _normalStyle = TextStyle(color: Colors.black, fontSize: 16);
  3. // 检索高亮文本样式
  4. TextStyle _highlightedStyle = TextStyle(color: Colors.green, fontSize: 16);
  5. Widget _text(String name) {
  6. List<TextSpan> spans = [];
  7. List<String> strTexts = name.split(_searchText);
  8. print(strTexts);
  9. if (strTexts.length > 0) {
  10. for (int i = 0; i < strTexts.length; i++) {
  11. String indexStr = strTexts[i];
  12. if (indexStr == '') {
  13. // 最后一行的末尾不用添加
  14. if (i < strTexts.length - 1) {
  15. spans.add(TextSpan(text: _searchText, style: _highlightedStyle));
  16. }
  17. } else {
  18. spans.add(TextSpan(text: indexStr, style: _normalStyle));
  19. // 最后一行字符的末尾不用添加
  20. if (i < strTexts.length - 1) {
  21. spans.add(TextSpan(text: _searchText, style: _highlightedStyle));
  22. }
  23. }
  24. }
  25. }
  26. return RichText(text: TextSpan(children: spans));
  27. }

image.png