在仿微信项目的首页微信界面顶部的搜索框还没有设计出来.所以我们这一章的目的是设计SeachBar与实现SearchPage.
一、自定义SeachCell
- 首先将微信聊天界面_ChatPageState类的build渲染方法下ListView.builder的itemBuilder实现部分抽离出来,形成一个回调方法,返回item给它.
Widget _buildItemForRow(BuildContext context,int index) {
return ListTile(
title: Text(_datas[index].name ?? ""),
subtitle: Container(
height: 20,width: 20,
child: Text(
_datas[index].message ?? "",
overflow: TextOverflow.ellipsis,
),
),
leading: CircleAvatar(
backgroundImage: NetworkImage(_datas[index].imageUrl ?? ""),
),
);
}
2. 接下来也就是,当当前的index == 0,也就是第一个元素的位置时,我们需要将SearchCell设置其上.也就是起展示作用,点击这个cell会跳转到SearchPage中.进行搜索的相关逻辑响应.
- 那么这里我们先将SearchCell实现.创建一个search_bar.dart文件.创建无状态组件SearchCell
import 'package:flutter/material.dart';
class SearchCell extends StatelessWidget {
const SearchCell({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(color: Colors.white,height: 54,);
}
}
3. 来到_ChatPageState类中,将私有方法 _buildItemForRow在index = 0的时候,设置上这个SearchCell;然后将网络数据相关的调用处设置为index-1.
4. 此时可以看到Cell被渲染了出来,那么我们来完善SearchCell的内部
- 整个Cell作为一个Container,背景色为主题灰色;
- 内部需要一个Container作为白色的底部
- 白色的底部有圆角, 所以采用装饰盒子BoxDecoration,内部设置borderRadius为6.0
- 白底中间需要一个Row布局,放置一个放大镜图片和‘搜索’文字.
- 又因为点击SearchCell需要进行跳转界面,所以需要用手势监听GestureDetector来包装,onTap作为回调.
GestureDetector(
onTap: () {
},
child: Container(
height: 44,
//设置内边距
padding: EdgeInsets.all(5),
color: WeChatThemeColor,
child: Container(
height: 34,
//白色的底部
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6.0),
),
//中间的搜索及图标
child: Row(
//设置主轴方向,让控件居中对齐
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image(image: AssetImage("images/放大镜b.png"),width: 15,color: Colors.grey,),
Text(' 搜索',style: TextStyle(fontSize: 15,color: Colors.grey ),),
],
),
),
),
)
- 综上: SearchCell完成,UI效果如下
二、定义SeachPage
- 点击SearchCell会跳转到SearchPage界面,创建一个SearchPage的类,然后开始分析SearchPage界面的布局元素.
- 顶部存在着一个有状态改变的SearchBar控件
- 下方为列表视图.
- 那么我们可以采用Column布局方式,包装SearchBar和ListView
- ListView采用灵活布局的方式填充界面.
Scaffold(
body: Column(
children: [
SearchBar(),
//灵活布局
Expanded(
//权重,占据所有空间
flex: 1,
child: MediaQuery.removePadding(
//移除刘海屏预留空间
removeTop: true,
context: context,
child: ListView.builder(
itemBuilder: (BuildContext context,int index){
return Text('123 ${index}');
}),
),
)
],
),
);
- 对应的SearchBar部分实现,先采用占位的方式体现
class _SearchBarState extends State<SearchBar> {
@override
Widget build(BuildContext context) {
return Container(
height: 84,
color: Colors.red,
);
}
}
- 从SearchCell跳转到SearchBar这个过程,需要用到Navigator作为中转.然后获取context,通过push的方式跳转到SearchPage.
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context ) => SearchPage())
);
},
三、布局SeachBar
- 根据搜索框的效果图来看,SearchBar的实现思路为:
- Container1包装一个Column列布局.
- Column中通过SizedBox填充40px、然后再以Container2的形式设置内容
- Container2中通过包含Row行布局方式,Row中包含了Container3和取消按钮.
- Container3也就是左边的圆角背景.一牵扯圆角,那么我们就采用BoxDecoration来包装实现.
- Container3中的child,采用Row行布局方式填充 放大镜图片、输入框、取消图标
- 那么Container1也就是整个搜索条部件的父视图
class _SearchBarState extends State<SearchBar> {
@override
Widget build(BuildContext context) {
return Container(
height: 84,
color: WeChatThemeColor,
child: Column(...),
);
}
}
- Container2实现为
- Container3实现部分为
- 以上就是对SearchBar的实现
四、SearchBar状态管理
- SearchBar上的事件响应,首先考虑‘取消’按钮的响应.如果点击了取消,那么我们应该将当前SearchPage pop回去,回到上一个界面.
- 考虑通过手势包装这个取消的Text.然后就实现了界面联动.
GestureDetector(
onTap: (){
Navigator.pop(context);
},
child: Text('取消'),//取消按钮,
)
2. 当我们没有输入内容的时候,右边清除按钮不会显示,也就是默认状态
- 当我们开始输入内容时,输入的文本与下方的ListView内容会有关联,这个时候是属于高亮状态.这样的话我们就需要个私有状态变量来标识当前状态.
- 然而当前我们先考虑监听当前文本框的输入,也就是需要一个文本编辑控制器TextEditingController
final TextEditingController _controller = TextEditController();
3. 将这个私有变量赋值给TextField的controller属性.相当于设置了个代理.
TextField(
//文本编辑控制器
controller: _controller,
...
),
4. 输入框中文本内容发生变化的时候,我们可以拿到输入的变化内容.也就是通过TextField的onChanged方法.因此我们封装个对应的方法.于此同时考虑清除按钮的显示隐藏控制
//是否显示清除按钮
bool _showClear = false;
_onChange(String text) {
if (text.length > 0) {
_showClear = true;
} else {
_showClear = false;
}
setState(() { });
}
- TextField处的调用为
TextField(onChanged: _onChange,...),
5. 对应的处理清除按钮相应的响应;
- 当点击清除的时候,要清除文本内容并且设置文本改变的回调传递‘’
//清除图标
GestureDetector(
onTap: (){
//清除文本内容
_controller.clear();
//设置文本改变的回调
_onChange('');
},
//如果当前清除标记为true,就显示清除按钮,否则不显示
child: _showClear ? Icon(Icons.cancel, size: 20,color: Colors.grey,)
:Container(),
),
6. 接下来要实现搜索的事件响应,有两种方式
- 第一种:我们直接在输入框输入内容后,在SearchBar中去检索内容,将内容返回出去.
- 第二种:拿到所有数据,将检索找到后的数据直接返回出去.
- 也就是说检索操作是在SearchBar中操作还是在外界使用的地方操作.根据设计原则,我们最好是在SearchPage中处理检索操作.SearchBar只是负责单一功能,输入内容,传递内容.这就是下一章节要处理的事件.
五、SearchBar事件响应
- 在微信首界面的SearchCell跳转进SearchPage的时候,我们可以将总数据源传递过来,方便后边的检索操作.
- 因此在SearchCell中,我们创建一个接收外界数据源的数组,然后完善构造方法.
class SearchCell extends StatelessWidget {
final List<Chat> datas;
const SearchCell({Key? key, required this.datas}) : super(key: key);
...
}
- 调用时传递总数据
2. 与此同时,在SearchPage中也需要一个总数据源的变量,在点击SearchCell时传递
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context ) => SearchPage(datas: datas,))
);
},
3. 在SearchPage中如何显示,根据搜索框输入的内容,通知SearchPage,SearchPage进行过滤,然后显示搜索结果.这个时候就需要搜索框SearchBar定义一个回调方法.
- 在SearchPage中调用
//通过回调拿到搜索的内容
SearchBar(onChanged: (String text){
print('收到了: ${text}');
_searchData(text);
}),
4. 当我们拿到了搜索的内容后,就需要做数据的检索操作,这个时候封装一个_searchData方法,并且将检索的结果存入一个模型数组中.那么这个模型数组就是我们要展示的数据了.
class _SearchPageState extends State<SearchPage> {
//显示内容根据模型数组来.这里存入检索的结果
List <Chat> _models = [];
//对搜索的内容进行检索
void _searchData(String str) {
if (str.isEmpty) {
_models = [];
} else {
_models.clear();
for(int i = 0; i < widget.datas.length; i++) {
if (widget.datas[i].name != null && widget.datas[i].name!.contains(str)) {
_models.add(widget.datas[i]);
}
}
}
setState((){});
}
.....
}
- ListView.builder中展示检索的名称
ListView.builder(
itemCount: _models.length,
itemBuilder: (BuildContext context,int index){
return Text('${_models[index].name}');
}
),
至此SearchBar的事件响应联动处理完毕,但是搜索发现中文名称,搜索结果显示太少,所以从后台将中文名称配置为英文名称(@cname改为@name);
六、SearchPage高亮显示
- 下面我们开始处理搜索的高亮状态,首先将搜索界面的布局填充.因为布局方式和首页相同,那直接填充过来.
Widget _buildItemForRow(BuildContext context,int index) {
return ListTile(
//对title采用富文本
title: Text(_models[index].name ?? ""),
subtitle: Container(
height: 20,width: 20,
child: Text(
_models[index].message ?? "",
overflow: TextOverflow.ellipsis,
),
),
leading: CircleAvatar(
backgroundImage: NetworkImage(_models[index].imageUrl ?? ""),
),
);
}
- itemBuilder调用处
MediaQuery.removePadding(
//移除刘海屏预留空间
removeTop: true,
context: context,
child: ListView.builder(
itemCount: _models.length,
itemBuilder: _buildItemForRow,
),
),MediaQuery.removePadding(
//移除刘海屏预留空间
removeTop: true,
context: context,
child: ListView.builder(
itemCount: _models.length,
itemBuilder: _buildItemForRow,
),
),
2. 在输入要搜索的文本时,检索到的内容应该选中为绿色.所以先拿到关键字.创建一个私有变量 _searchStr;在_searchData收到搜索框改变的方法中进行赋值作为记录.
3. 因为要显示高亮状态,那么需要将title抽离出来,形成一个封装
- 然后做展示.字体显示方面,会有默认状态和高亮状态两种形式.
- 通过TextStyle定义两种风格.然后根据搜索记录值,将标题进行分割.
- 找到哪些是高亮的,哪些是默认状态的,通过TextSpan设置好,最后返回设置好的富文本.
Widget _handleTitle(String name) {
//默认黑色样式
TextStyle normalStyle = TextStyle(fontSize: 16,color: Colors.black);
//高亮状态下的绿色
TextStyle highlightStyle = TextStyle(fontSize: 16,color: Colors.green);
List<TextSpan> spas = [];
//通过搜索的字符串将原name字符串进行分割.
List<String> strs = name.split(_searchStr) as List<String>;
//找到哪些是高亮,哪些是默认的
for(int i = 0;i < strs.length;i++){
spas.add(TextSpan(text: strs[i]));
}
//返回富文本
return RichText(
text: TextSpan(children: spas),
);
}
4. 来到flutter快速查看运行结果的网站,编写截取部分的逻辑写入其中,输入特定值,查看运行效果
void main(){
List<String> strs = "hahasfsawerhaa".split('a') ;
//找到哪些是高亮,哪些是默认的
for(int i = 0;i < strs.length;i++){
print("-${strs[i]}-");
}
}
- 输出结果
-h-
-h-
-sfs-
-werh-
--
--
- 实践多次,得出结论
- 遇到空串就是高亮,
- 遇到字符后边就跟着高亮,
- 如果字符是结尾处就不用跟着高亮.
5. 接下来根据结论完善内部逻辑.
-
- 设置默认样式和高亮样式
- 通过搜索的字符串将原name进行分割
- 找到哪些是高亮,哪些是默认
- 如果是空串,后边就跟着高亮状态;并且当前不是最后一个
- 如果不是空串,后边首先接着正常的文本
- 只要不是最后一个,紧接着就是分割后的高亮状态
- 返回处理后的富文本
Widget _handleTitle(String name) {
//默认黑色样式
TextStyle normalStyle = TextStyle(fontSize: 16,color: Colors.black);
//高亮状态下的绿色
TextStyle highlightStyle = TextStyle(fontSize: 16,color: Colors.green);
List<TextSpan> spans = [];
//通过搜索的字符串将原name字符串进行分割.
List<String> strs = name.split(_searchStr) as List<String>;
//找到哪些是高亮,哪些是默认的
for(int i = 0;i < strs.length;i++){
String str = strs[i];
//如果是空串,后边就跟着高亮状态;并且当前不是最后一个
if (str == '' && i < strs.length - 1){
spans.add(TextSpan(text: _searchStr, style: highlightStyle));
} else {
//如果不是空串,后边首先接着正常的文本
spans.add(TextSpan(text: str, style: normalStyle));
//只要不是最后一个,紧接着就是分割后的高亮状态
if (i < strs.length-1){
spans.add(TextSpan(text: _searchStr,style: highlightStyle));
}
}
}
//返回富文本
return RichText(
text: TextSpan(children: spans),
);
}
6.现在还存在一个问题,当滚动当前ListView时,需要将键盘撤回.那么实现为
- 效果如下:
综上: 整个仿微信项目完成,笔记+代码耗时5天左右.