Flutter模仿微信通讯录列表、列表筛选

简书地址

用flutter模仿微信通讯录列表写的一个demo,具体效果看gif图、文章末尾有demo地址

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传送门

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你可以使用 `GestureDetector` 组件来监听长按事件,并在长按时显示一个底部的菜单栏,用于选择多个消息。下面是一个简单的示例代码: ```dart class ChatMessage { final String content; bool selected; ChatMessage(this.content, {this.selected = false}); } class ChatScreen extends StatefulWidget { @override _ChatScreenState createState() => _ChatScreenState(); } class _ChatScreenState extends State<ChatScreen> { final List<ChatMessage> messages = [ ChatMessage("Hello!"), ChatMessage("How are you?"), ChatMessage("I'm fine, thanks."), ChatMessage("What about you?"), ChatMessage("I'm good."), ]; List<ChatMessage> selectedMessages = []; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Chat"), ), body: ListView.builder( itemCount: messages.length, itemBuilder: (context, index) { final message = messages[index]; return GestureDetector( onLongPress: () { setState(() { message.selected = true; selectedMessages.add(message); }); }, child: Container( padding: EdgeInsets.all(16.0), color: message.selected ? Colors.grey[300] : null, child: Text(message.content), ), ); }, ), bottomNavigationBar: selectedMessages.isNotEmpty ? BottomAppBar( child: Row( children: [ IconButton( icon: Icon(Icons.delete), onPressed: () { setState(() { messages.removeWhere((message) => selectedMessages.contains(message)); selectedMessages.clear(); }); }, ), Text("${selectedMessages.length} selected"), ], ), ) : null, ); } } ``` 在这个示例中,我们创建了一个 `ChatMessage` 类来表示聊天消息。`selected` 属性表示该消息是否被选中。在 `ChatScreen` 中,我们创建了一个 `messages` 列表来存储所有的消息,以及一个 `selectedMessages` 列表来存储被选中的消息。在 `ListView.builder` 中,我们使用 `GestureDetector` 来监听长按事件,并在长按时将消息标记为选中状态,并将其添加到 `selectedMessages` 列表中。在底部菜单栏中,我们显示了一个删除按钮和一个文本框,用于显示选中的消息数量。当用户点击删除按钮时,我们从 `messages` 列表中删除所有被选中的消息,并清空 `selectedMessages` 列表
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值