Flutter实战-搭建微信项目(九)

1,141 阅读4分钟

本篇最终效果

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

image.png

分析思路

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

方法抽取的快捷键command+option+M,弹出对话框输入方法名称Refactor就ok了

image.png

在这里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!),
      ),
    );
  }

image.png

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');
                  })
          )
        ],
      ),
    );
  }
}

此时运行效果如下:

image.png

我们发现ListView上面有一点边距,这样写来去掉吧:

  Expanded(
    child: MediaQuery.removePadding(
             context: context,
             removeTop: true,
             child: ListView.builder(
                      itemCount: 10,
                      itemBuilder: (BuildContext context, int index) {
                        return Text('123$index');
                      }
                   )
          )
   )

image.png

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('取消')
              ],
            ),
          )
        ],
      ),
    );
  }
}

运行之后的效果:

image.png

接着上面,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,
    )
 ],
),

image.png

可以明显的发现几个问题

  • 左右间距太近
  • 光标的颜色不对
  • 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)
  )
)

image.png

继续补充完细节:当点击取消的时候,返回上一级页面,以及当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(() {});
  }

image.png

还有一点没有完善就是输入的文本,在对应的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));
 }