本篇最终效果
在首页新增一个搜索条,点击的时候跳转到新的页面,输入能够实时检索。
分析思路
这里的searchBar
是跟着ListView
可以上下一起移动的,所以把这个搜索框放入ListView
里, 那么itemCount: _datas.length + 1,
准备改造下itemBuilder
,所以先把这里抽取出来。
方法抽取的快捷键command+option+M
,弹出对话框输入方法名称Refactor
就OK了
在这里index=0
的时候加入一个搜索框,为了保证后面的数据不紊乱,需要index--
Widget _itemBuilderForRow(BuildContext context, int index) {
if (index == 0) {
return Container(color: Colors.red, height: 44);
}
index--;
return ListTile(
title: Text(_datas[index].name!),
subtitle: Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(right: 10),
height: 20,
child: Text(
_datas[index].message!,
overflow: TextOverflow.ellipsis,
),
),
leading: CircleAvatar(
backgroundImage: NetworkImage(_datas[index].imgUrl!),
),
);
}
SearchCell
能触发点击的可以使用GestureDetector
,布局方面使用Stack + row
混合搭配使用就能达到
import 'package:flutter/material.dart';
class SearchCell extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Container(
color: Color.fromRGBO(238, 238, 238, 1),
height: 44,
padding: EdgeInsets.all(5),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(5)),
),
Container(
height: 44,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'images/放大镜b.png',
width: 15,
color: Colors.grey,
),
Text(
' 搜索 ',
style: TextStyle(fontSize: 15, color: Colors.grey),
)
],
),
)
],
),
),
);
}
}
SearchPage
首先这个搜索页面是有状态的,同时是一个没有AppBar的一个Column布局的一个视图。上面是一个SearchBar
,下面是一个ListView且支持富文本显示。
所以在SearchCell点击的时候,跳转新页面
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) => SearchPage()));
在SearchPage
页面布局如下,我们可以给Scaffold
不设置appBar属性
class _SearchPageState extends State<SearchPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
SearchBar(),
Expanded(
child: ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Text('123$index');
})
)
],
),
);
}
}
此时运行效果如下
我们发现ListView上面有一点边距,这样写来去掉吧:
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Text('123$index');
})))
SearchBar
分析:这里的SearchBar整体是一个Column结构,上面是一个占位的SizeBox,下面是一个Container,Container内是一个Row布局的一个圆角装饰器和右边的一个Text
class _SearchBarState extends State<SearchBar> {
@override
Widget build(BuildContext context) {
return Container(
height: 84,
color: Colors.red,
child: Column(
children: [
SizedBox(height: 40),
Container(
height: 44,
color: Colors.grey,
child: Row(
children: [
Container(
margin: EdgeInsets.only(left: 6, right: 6),
width: screenWidth(context) - 45,
height: 34,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Colors.white),
),
Text('取消')
],
),
)
],
),
);
}
}
运行之后的效果:
接着上面,Container内继续Row布局,左边是一个搜索的图片,中间是一个TextField,右边是一个clear的按钮
Row(
children: [
Image.asset('images/放大镜b.png',
width: 20, color: Colors.grey),
Expanded(
child: TextField()),
Icon(
Icons.clear,
color: Colors.grey,
size: 20,
)
],
),
可以明显的发现几个问题
- 左右间距太近
- 光标的颜色不对
- TextField边框有颜色
- 文本输入的位置偏下,没有居中
- 没有默认的占位文字
针对以上的问题,补充以下细节:
padding: EdgeInsets.only(left: 6, right: 6),
TextField(
cursorColor: Colors.green,
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 16,
),
decoration: InputDecoration(
hintText: '搜索',
border: InputBorder.none,
contentPadding: EdgeInsets.only(bottom: 12, left: 6)
)
)
继续补充完细节:当点击取消的时候,返回上一级页面,以及当TextView有字时,clear按钮展示,反之则不展示。可以通过监听TextField的onChange
的回调,这里其实跟iOS有异曲同工之妙typedef ValueChanged<T> = void Function(T value);
,新增一个变量_showClear
来记录是否展示。在布局的时候,如果_showClear
为真就展示,否则给一个空白的Container()
void _onChanged(String value) {
setState(() {
_showClear = value.length > 0
});
}
接着,当我们点击clear按钮的时候,需要清空当前的文本输入框,所以需要给clear按钮添加一个点击事件。
if (_showClear)
GestureDetector(
onTap: () {
_controller.clear();
_onChanged('');
},
child: Icon(
Icons.clear,
color: Colors.grey,
size: 20,
),
)
好了,到了现在已经实现了输入文本框的时候,清除按钮展示反之则隐藏。并且点击清除按钮的时候,文本框会同步清空。
数据处理
值传递:ChatPage.datas
==> SearchCell.datas
==>SearchPage.datas
回调:在初始化SearchBar
的时候,定义一个回调,有点类似于闭包。
final ValueChanged<String>? onChanged;
const SearchBar({this.onChanged});
然后在TextField的onChanged
的时候,调用这个函数
if (widget.onChanged != null) {
widget.onChanged!(value);
}
最后在SearchPage
使用也就是初始化SearchBar
的地方回调
SearchBar(onChanged: (value) {
print('搜索$value');
}),
这样就能实时监听SearchBar的文本输入,同时来检索SearchPage.datas
中的数据。检索的方法还是比较简单的,值得注意的是在每次检索之后调用setState
来更新数据。
List<ChatModel> _models = [];
void _searchData(String value) {
_models.clear();
if (value.length > 0 && widget.data != null) {
for (int i = 0; i < widget.data!.length; i++) {
String name = widget.data![i].name ?? '';
print(name);
if (name.contains(value)) _models.add(widget.data![i]);
}
}
setState(() {});
}
此时效果现在如下:
还有一点没有完善就是输入的文本,在对应的cell里面需要高亮显示,我们继续来实现吧!
针对这里的Text,我们可以定义一个专门的方法来自定义返回。这里我采用的是split
方法。
// 正常情况下的文本样式
TextStyle _normalStyle = TextStyle(color: Colors.black, fontSize: 16);
// 检索高亮文本样式
TextStyle _highlightedStyle = TextStyle(color: Colors.green, fontSize: 16);
Widget _text(String name) {
List<TextSpan> spans = [];
List<String> strTexts = name.split(_searchText);
print(strTexts);
if (strTexts.length > 0) {
for (int i = 0; i < strTexts.length; i++) {
String indexStr = strTexts[i];
if (indexStr == '') {
// 最后一行的末尾不用添加
if (i < strTexts.length - 1) {
spans.add(TextSpan(text: _searchText, style: _highlightedStyle));
}
} else {
spans.add(TextSpan(text: indexStr, style: _normalStyle));
// 最后一行字符的末尾不用添加
if (i < strTexts.length - 1) {
spans.add(TextSpan(text: _searchText, style: _highlightedStyle));
}
}
}
}
return RichText(text: TextSpan(children: spans));
}