美文网首页
Flutter模仿微信通讯录列表

Flutter模仿微信通讯录列表

作者: Tomous | 来源:发表于2023-11-10 22:43 被阅读0次

    用flutter模仿微信通讯录列表写的一个demo,具体效果看gif图

    list .gif
    下面附上代码和 Demo,注释写的还是比较清楚的,这里就不做一一介绍了,

    入口是ListPage类,

    import 'dart:convert';
    
    import 'package:flutter/material.dart';
    import 'package:list/list/model/user_name.dart';
    import 'package:list/list/pages/common.dart';
    import 'package:list/list/pages/index_bar.dart';
    import 'package:list/list/pages/item_cell.dart';
    import 'package:list/list/pages/search_widget.dart';
    
    class ListPage extends StatefulWidget {
      const ListPage({super.key});
    
      @override
      State<ListPage> createState() => _ListPageState();
    }
    
    class _ListPageState extends State<ListPage> {
      List<DataList> _data = [];
      final List<DataList> _dataList = []; //数据
      late ScrollController _scrollController;
      //字典 里面放item和高度对应的数据
      final Map<String, double> _groupOffsetMap = {
        INDEX_WORDS[0]: 0.0, //放大镜
        INDEX_WORDS[1]: 0.0, //⭐️
      };
      String searchStr = '';
      @override
      void initState() {
        _load();
        super.initState();
        _scrollController = ScrollController();
      }
    
      @override
      void dispose() {
        _scrollController.dispose();
        super.dispose();
      }
    
      void _load() async {
        String jsonData = await loadJsonFromAssets('assets/data.json');
        Map<String, dynamic> dict = json.decode(jsonData);
        List<dynamic> list = dict['data_list'];
        _data = list.map((e) => DataList.fromJson(e)).toList();
        // 排序
        _data.sort((a, b) => a.indexLetter.compareTo(b.indexLetter));
    
        _dataList.addAll(_data);
        // 循环计算,将每个头的位置算出来,放入字典
        var groupOffset = 0.0;
        for (int i = 0; i < _dataList.length; i++) {
          if (i < 1) {
            //第一个cell一定有头
            _groupOffsetMap.addAll({_dataList[i].indexLetter: groupOffset});
            groupOffset += cellHeight + cellHeaderHeight;
          } else if (_dataList[i].indexLetter == _dataList[i - 1].indexLetter) {
            // 相同的时候只需要加cell的高度
            groupOffset += cellHeight;
          } else {
            //第一个cell一定有头
            _groupOffsetMap.addAll({_dataList[i].indexLetter: groupOffset});
            groupOffset += cellHeight + cellHeaderHeight;
          }
        }
        print('dc------$_groupOffsetMap');
        setState(() {});
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: const Text('通讯录'),
          ),
          body: Stack(
            children: [
              //列表
              Column(
                children: [
                  // 搜索框
                  SearchWidget(
                    onSearchChange: (text) {
                      _dataList.clear();
                      searchStr = text;
                      if (text.isNotEmpty) {
                        for (int i = 0; i < _data.length; i++) {
                          String name = _data[i].name;
                          if (name.contains(text)) {
                            _dataList.add(_data[i]);
                          }
                        }
                      } else {
                        _dataList.addAll(_data);
                      }
                      setState(() {});
                    },
                  ),
                  Expanded(
                    child: ListView.builder(
                      controller: _scrollController,
                      itemCount: _dataList.length,
                      itemBuilder: _itemForRow,
                    ),
                  ),
                ],
              ),
              // 索引条
              Positioned(
                right: 0.0,
                top: screenHeight(context) / 8,
                height: screenHeight(context) / 2,
                width: indexBarWidth,
                child: IndexBarWidget(
                  indexBarCallBack: (str) {
                    print('拿到索引条选中的字符:$str');
                    if (_groupOffsetMap[str] != null) {
                      _scrollController.animateTo(
                        _groupOffsetMap[str]!,
                        duration: const Duration(microseconds: 100),
                        curve: Curves.easeIn,
                      );
                    } else {}
                  },
                ),
              ),
            ],
          ),
        );
      }
    
      Widget? _itemForRow(BuildContext context, int index) {
        DataList user = _dataList[index];
        //是否显示组名字
        bool hiddenTitle = index > 0 &&
            _dataList[index].indexLetter == _dataList[index - 1].indexLetter;
        return ItemCell(
          imageUrl: user.imageUrl,
          name: user.name,
          groupTitle: hiddenTitle ? null : user.indexLetter,
        );
      }
    }
    

    这是每个cell的内容显示

    import 'package:flutter/material.dart';
    import 'package:list/list/pages/common.dart';
    
    class ItemCell extends StatelessWidget {
      final String? imageUrl;
      final String name;
      final String? groupTitle;
      const ItemCell({
        super.key,
        this.imageUrl,
        required this.name,
        this.groupTitle,
      });
    
      @override
      Widget build(BuildContext context) {
        // TextStyle normalStyle = const TextStyle(
        //   fontSize: 16,
        //   color: Colors.black,
        // );
        // TextStyle highlightStyle = const TextStyle(
        //   fontSize: 16,
        //   color: Colors.green,
        // );
        return Column(
          children: [
            Container(
              alignment: Alignment.centerLeft,
              padding: const EdgeInsets.only(left: 10.0),
              height: groupTitle != null ? cellHeaderHeight : 0.0,
              color: Colors.grey,
              child: groupTitle != null ? Text(groupTitle!) : null,
            ),
            SizedBox(
              height: cellHeight,
              child: ListTile(
                leading: Container(
                  width: 40,
                  height: 40,
                  decoration: BoxDecoration(
                    color: Colors.red,
                    image: imageUrl == null
                        ? null
                        : DecorationImage(
                            image: NetworkImage(imageUrl!),
                            fit: BoxFit.cover,
                          ),
                    borderRadius: const BorderRadius.all(
                      Radius.circular(4),
                    ),
                  ),
                ),
                title: _title(name),
              ),
            ),
          ],
        );
      }
    
      Widget _title(String name) {
         //这里是让搜索的字体显示高亮状态
        // List<TextSpan> spans = [];
        // List<String> strs = name.split(searchStr);
        // 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));
        return Text(name);
      }
    }
    

    这是搜索框

    import 'package:flutter/material.dart';
    import 'package:list/list/pages/common.dart';
    
    class SearchWidget extends StatefulWidget {
      final void Function(String) onSearchChange;
      const SearchWidget({
        super.key,
        required this.onSearchChange,
      });
    
      @override
      State<SearchWidget> createState() => _SearchWidgetState();
    }
    
    class _SearchWidgetState extends State<SearchWidget> {
      bool _isShowClear = false;
      final TextEditingController _textEditingController = TextEditingController();
      @override
      void dispose() {
        _textEditingController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 44,
          color: Colors.red,
          child: Row(
            children: [
              Container(
                width: screenWidth(context) - 20,
                height: 34,
                margin: const EdgeInsets.only(left: 10, right: 10.0),
                padding: const EdgeInsets.only(left: 10, right: 10.0),
                decoration: const BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.all(Radius.circular(6)),
                ),
                child: Row(
                  children: [
                    const Icon(Icons.search),
                    Expanded(
                      child: TextField(
                        onChanged: _onChange,
                        controller: _textEditingController,
                        decoration: const InputDecoration(
                          hintText: '请输入搜索内容',
                          border: InputBorder.none,
                          contentPadding: EdgeInsets.only(
                            left: 10,
                            bottom: 12,
                          ),
                        ),
                      ),
                    ),
                    if (_isShowClear)
                      GestureDetector(
                        onTap: () {
                          _textEditingController.clear();
                          setState(() {
                            _onChange('');
                          });
                        },
                        child: const Icon(Icons.cancel),
                      ),
                  ],
                ),
              ),
            ],
          ),
        );
      }
    
      _onChange(String text) {
        _isShowClear = text.isNotEmpty;
        widget.onSearchChange(text);
      }
    }
    

    这是右侧的索引条

    import 'package:flutter/material.dart';
    import 'package:list/list/pages/common.dart';
    
    class IndexBarWidget extends StatefulWidget {
      final void Function(String str) indexBarCallBack;
      const IndexBarWidget({
        super.key,
        required this.indexBarCallBack,
      });
    
      @override
      State<IndexBarWidget> createState() => _IndexBarWidgetState();
    }
    
    class _IndexBarWidgetState extends State<IndexBarWidget> {
      Color _bkColor = const Color.fromRGBO(1, 1, 1, 0.0);
      Color _textColor = Colors.black;
    
      double _indicatorY = 0.0;
      String _indicatorStr = 'A';
      bool _indicatorShow = false;
      @override
      void initState() {
        super.initState();
      }
    
    // 获取选中的字符
      int getIndex(BuildContext context, Offset globalPosition) {
        // 拿到点前小部件(Container)的盒子
        RenderBox renderBox = context.findRenderObject() as RenderBox;
        // 拿到y值
        double y = renderBox.globalToLocal(globalPosition).dy;
        // 算出字符高度
        double itemHeight = renderBox.size.height / INDEX_WORDS.length;
        // 算出第几个item
        // int index = y ~/ itemHeight;
        // 为了防止滑出区域后出现问题,所以index应该有个取值范围
        int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
        return index;
      }
    
      @override
      Widget build(BuildContext context) {
        //索引条
        final List<Widget> wordsList = [];
        for (var i = 0; i < INDEX_WORDS.length; i++) {
          wordsList.add(
            Expanded(
              child: Text(
                INDEX_WORDS[i],
                style: TextStyle(
                  color: _textColor,
                  fontSize: 14.0,
                ),
              ),
            ),
          );
        }
        return Row(
          children: [
            Container(
              alignment: Alignment(0.0, _indicatorY),
              width: indexBarWidth - 20.0,
              // color: Colors.red,
              child: _indicatorShow
                  ? Stack(
                      alignment: const Alignment(-0.1, 0),
                      children: [
                        //应该放一张图片,没找到合适的,就用Container代替
                        Container(
                          width: 60.0,
                          height: 60.0,
                          decoration: const BoxDecoration(
                            color: Colors.green,
                            borderRadius: BorderRadius.all(
                              Radius.circular(30.0),
                            ),
                          ),
                        ),
                        Text(
                          _indicatorStr,
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 28.0,
                            fontWeight: FontWeight.bold,
                          ),
                        )
                      ],
                    )
                  : null,
            ),
            GestureDetector(
              onVerticalDragDown: (details) {
                int index = getIndex(context, details.globalPosition);
                widget.indexBarCallBack(INDEX_WORDS[index]);
                setState(() {
                  _bkColor = const Color.fromRGBO(1, 1, 1, 0.5);
                  _textColor = Colors.white;
    
                  //显示气泡
                  _indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
                  _indicatorStr = INDEX_WORDS[index];
                  _indicatorShow = true;
                });
              },
              onVerticalDragEnd: (details) {
                setState(() {
                  _bkColor = const Color.fromRGBO(1, 1, 1, 0.0);
                  _textColor = Colors.black;
    
                  // 隐藏气泡
                  _indicatorShow = false;
                });
              },
              onVerticalDragUpdate: (details) {
                int index = getIndex(context, details.globalPosition);
                widget.indexBarCallBack(INDEX_WORDS[index]);
    
                //显示气泡
                setState(() {
                  _indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
                  _indicatorStr = INDEX_WORDS[index];
                  _indicatorShow = true;
                });
              },
              child: Container(
                color: _bkColor,
                width: 20.0,
                child: Column(
                  children: wordsList,
                ),
              ),
            ),
          ],
        );
      }
    }
    
    

    这是定义的宏

    //cell头的高度
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    const double cellHeaderHeight = 30.0;
    // cell的高度
    const double cellHeight = 50.0;
    // cell的高度
    const double indexBarWidth = 130.0;
    
    double screenWidth(BuildContext context) => MediaQuery.of(context).size.width;
    double screenHeight(BuildContext context) => MediaQuery.of(context).size.height;
    
    const INDEX_WORDS = [
      '🔍',
      '⭐️',
      'A',
      'B',
      'C',
      'D',
      'E',
      'F',
      'G',
      'H',
      'I',
      'J',
      'K',
      'L',
      'M',
      'N',
      'O',
      'P',
      'Q',
      'R',
      'S',
      'T',
      'U',
      'V',
      'W',
      'X',
      'Y',
      'Z',
    ];
    
    //从本地加载json数据
    Future<String> loadJsonFromAssets(String fileName) async {
      return await rootBundle.loadString(fileName);
    }
    
    

    demo传送门

    相关文章

      网友评论

          本文标题:Flutter模仿微信通讯录列表

          本文链接:https://www.haomeiwen.com/subject/eolkwdtx.html