7、Flutter项目的SeachPage

在仿微信项目的首页微信界面顶部的搜索框还没有设计出来.所以我们这一章的目的是设计SeachBar与实现SearchPage.

一、自定义SeachCell

  1. 首先将微信聊天界面_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状态管理

  1. 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事件响应

  1. 在微信首界面的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高亮显示

  1. 下面我们开始处理搜索的高亮状态,首先将搜索界面的布局填充.因为布局方式和首页相同,那直接填充过来.
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天左右.

猜你喜欢

转载自blog.csdn.net/SharkToping/article/details/130515442