IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Flutter (仿微信通讯录)按字母分组列表 -> 正文阅读

[移动开发]Flutter (仿微信通讯录)按字母分组列表

再开发过程中我们经常会用到按字母顺序将名称惊醒分组,并且在列表最右侧有指示器,效果图如下。这个效果也是我参照一位大佬的博客才实现的,不过忘记了大佬博客的链接,还是很感谢这位大神的。下面是我自己整理的代码和效果。

今天我们就来讲解一下这个效果的实现,还是老规矩,直接上代码讲解,这里我是通过三个类来实现的,分别主页ChildrenList、右侧指示器IndexBar还有一个数据模型类Friends


首先讲一下Friends数据模型类

class Friends {
  final String imageUrl;
  final String name;
  final String indexLetter; //首字母大写

  Friends({this.imageUrl, this.name, this.indexLetter});
}

List<Friends> datas = [
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/27.jpg',
      name: 'Lina',
      indexLetter: 'L'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/17.jpg',
      name: '菲儿',
      indexLetter: 'F'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/16.jpg',
      name: '安莉',
      indexLetter: 'A'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/men/31.jpg',
      name: '阿贵',
      indexLetter: 'A'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/22.jpg',
      name: '贝拉',
      indexLetter: 'B'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/37.jpg',
      name: 'Lina',
      indexLetter: 'L'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/18.jpg',
      name: 'Nancy',
      indexLetter: 'N'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/men/47.jpg',
      name: '扣扣',
      indexLetter: 'K'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/men/3.jpg',
      name: 'Jack',
      indexLetter: 'J'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/5.jpg',
      name: 'Emma',
      indexLetter: 'E'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/24.jpg',
      name: 'Abby',
      indexLetter: 'A'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/men/15.jpg',
      name: 'Betty',
      indexLetter: 'B'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/men/13.jpg',
      name: 'Tony',
      indexLetter: 'T'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/men/26.jpg',
      name: 'Jerry',
      indexLetter: 'J'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/men/36.jpg',
      name: 'Colin',
      indexLetter: 'C'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/12.jpg',
      name: 'Haha',
      indexLetter: 'H'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/11.jpg',
      name: 'Ketty',
      indexLetter: 'K'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/13.jpg',
      name: 'Lina',
      indexLetter: 'L'),
  Friends(
      imageUrl: 'https://randomuser.me/api/portraits/women/23.jpg',
      name: 'Lina',
      indexLetter: 'L'),
];

?这个类我就不过多解释了。就是数据转model为了方便操作。


下来就是指示器的IndexBar

这里有两个地方要注意一下,

import 'package:nursery_school_gardener/util/ColorUtils.dart';
import 'package:nursery_school_gardener/util/FontSizeUtils.dart';

?这两个类,是我自定义的颜色类和字体类,为了方便UI处理,大家可以根据自己的需要进行替换就可以了或者直接使用系统的颜色和字体

import 'package:flutter/cupertino.dart';
import 'package:nursery_school_gardener/constant/DataBase.dart';
import 'package:nursery_school_gardener/util/ColorUtils.dart';
import 'package:nursery_school_gardener/util/FontSizeUtils.dart';

import 'friends_data.dart';
// 重点是 sonKey 变量,这个是为了父类调用子类使用的,因为我们在滑动名称列表的时候也需要改变指示器的位置,这个时候就需要反向改变指示器的位置
GlobalKey<_IndexBarState> sonKey = GlobalKey();

class IndexBar extends StatefulWidget {
  //创建索引条回调
  final void Function(String str) indexBarCallBack;
    //重写构造方法
  IndexBar({Key key,this.indexBarCallBack}) : super(key: key);
  @override
  _IndexBarState createState() => _IndexBarState();
}

int getIdex(BuildContext context, Offset globalPosition, List index_word) {
//  拿到box
  RenderBox box = context.findRenderObject();
//  拿到y值
  double y = box.globalToLocal(globalPosition).dy;
//  算出字符高度  box 的总高度 / 2 / 字符开头数组个数
  var itemHeight = MediaQuery.of(context).size.height / 2 / index_word.length;
  //算出第几个item,并且给一个取值范围   ~/ y除以item的高度取整  clamp 取值返回 0 -
  int index = (y ~/ itemHeight).clamp(0, index_word.length - 1);
  print('现在选中的是${index_word[index]}');

  return index;
}

final List<String> _index_word = [];

class _IndexBarState extends State<IndexBar> {
  Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
  double _indicatorY = 0.0; //悬浮窗位置
  String _indicatorText = 'A'; //显示的字母
  bool _indocatorHidden = true; //是否隐藏悬浮窗
  int indexSelect = 0;
//  排序后的数组
  final List<Friends> _listDatas = [];


  // 名称列表在滑动到某些位置是,会将指示器的位置也返回过来,然后刷新指示器选择位置
  void setIndexSelect(int index) {
    setState(() {
      indexSelect = index;
    });
  }

  void initState() {
    super.initState();
//----------------------- 1 -------------------------------
//    1、根据实际数据显示右侧bar
    _listDatas.clear();
    _index_word.clear();
    _listDatas.addAll(datas);
    //排序!
    _listDatas.sort((Friends a, Friends b) {
      return a.indexLetter.compareTo(b.indexLetter);
    });
    //经过循环,将每一个头的首字母放入index_word数组
    for (int i = 0; i < _listDatas.length; i++) {
      if (i < 1 || _listDatas[i].indexLetter != _listDatas[i - 1].indexLetter) {
        _index_word.add(_listDatas[i].indexLetter);
      }
    }
    //----------------------- 2 -------------------------------
    // 2、右侧bar显示全部字母
    // _index_word.addAll(INDEX_WORDS);
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> words = [];
    for (int i = 0; i < _index_word.length; i++) {
      words.add(
        Expanded(
          child: Container(
            alignment: Alignment.center,
            // 设置指示器字母的大小和样式
            child: Container(
              height: 16,
              width: 16,
              alignment: Alignment.center,
              decoration: BoxDecoration(
                // 选择哪个字母,改变其颜色和背景色,同时刷洗其他为选择的字母样式
                color: indexSelect == i ? ColorUtils.WORKBENCH_MENU_BOTTOM : ColorUtils.CLEAR,
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                _index_word[i],
                style: TextStyle(fontSize: 10, color: indexSelect == i ? ColorUtils.WHITE : ColorUtils.TEXT_BLACK),
              ),
            ),
          ),
        ),
      );
    }

    return Positioned(
      right: 0.0,// 设置指示器的距顶高度高度、宽度等
      height: MediaQuery.of(context).size.height / 2,
      top: MediaQuery.of(context).size.height / 4 -
          DataBase.STATUS_BAR_HEIGHT -
          40,
      width: 120,
      child: Row(
        children: <Widget>[
          Container(
            alignment: Alignment(0, _indicatorY),
            width: 100,
            child: _indocatorHidden // 这里是设置气泡提示的,当滑动的时候会在屏幕中央位置有一个更大的字母提示
                ? null
                : Stack(
                    alignment:
                        Alignment(-0.2, 0), //0, 0 是中心  顶部是 0,-1  左边中心是-1,0
                    children: <Widget>[
                      Text(
                        _indicatorText,
                        style: TextStyle(
                            fontSize: FontSizeUtils.FONT_SIZE_40,
                            color: ColorUtils.BLUE_NORMAL),
                      ),
                    ],
                  ), //气泡
          ),
          GestureDetector(
            child: Container(
              width: 20,
              color: _bkColor,
              child: Column(
                children: words,
              ),
            ),
            // 当开始滑动指示器的时候,根据坐标计算出滑动到数组_index_word哪个字母位置,并将字母返回到名称页面
            onVerticalDragUpdate: (DragUpdateDetails details) {
              int index = getIdex(context, details.globalPosition, _index_word);
              indexSelect = index;
              setState(() {
                _indicatorText = _index_word[index];
                //根据我们索引条的Alignment的Y值进行运算的。从 -1.1 到 1.1
                //整个的Y包含的值是2.2
                _indicatorY = 2.2 / _index_word.length * index - 1.1;
                _indocatorHidden = false;
              });
              widget.indexBarCallBack(_index_word[index]);
            }, //按住屏幕移动手指实时更新触摸的位置坐标
            // 当开始点击指示器字母的时候出发,和滑动差不多
            onVerticalDragDown: (DragDownDetails details) {
              //globalPosition 自身坐标系
              int index = getIdex(context, details.globalPosition, _index_word);
              _indicatorText = _index_word[index];

              _indicatorY = 2.2 / _index_word.length * index - 1.1;
              _indocatorHidden = true;
              widget.indexBarCallBack(_index_word[index]);
              print('现在点击的位置是${details.globalPosition}');
              print('现在点击的位置是${indexSelect}');
              indexSelect = index;
              setState(() {
                _bkColor = ColorUtils.CLEAR;
              });
            }, //触摸开始
            // 当松开指示器的时候,指示器回复原样
            onVerticalDragEnd: (DragEndDetails details) {
              setState(() {
                _indocatorHidden = true;
                _bkColor = ColorUtils.CLEAR;
              }); //触摸结束
            },
          ) //这个是索引条
        ],
      ),
    );
  }
}

// 所有的字母
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'
];

最后是名称列表页面ChildrenList

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:nursery_school_gardener/util/ColorUtils.dart';//上面说的颜色类,删除也可
import 'package:nursery_school_gardener/util/FontSizeUtils.dart';//上面说的字体类,删除也可
import 'package:nursery_school_gardener/util/callBack/TitleBarCallBack.dart';//导航点击事件回掉,可删除
import 'package:nursery_school_gardener/view/main/my/archives/ChildrenFiles.dart';//没用删除
import 'package:nursery_school_gardener/widget/CustemSearch.dart';
import 'package:nursery_school_gardener/widget/CustomScaffold.dart';//就是Scaffold,只不过进行了封装更方便使用,前面的文章又介绍,使用Scaffold也可以
import 'package:nursery_school_gardener/widget/TitleBar.dart';//自定义导航,可直接删除

import 'IndexBar.dart';
import 'friends_data.dart';

/*
* 班级幼儿列表
* */
class ChildrenList extends StatefulWidget {
  @override
  _ChildrenListState createState() => _ChildrenListState();
}

class _ChildrenListState extends State<ChildrenList>
    implements TitleBarCallBack {
//  字典里面放item和高度的对应数据
  final Map _groupOffsetMap = {
//    这里因为根据实际数据变化和固定全部字母前两个值都是一样的,所以没有做动态修改,如果不一样记得要修改
    INDEX_WORDS[0]: 0.0,
    INDEX_WORDS[1]: 0.0,
  };
    
  ScrollController _scrollController;
  //第几个字母位置
  int indexSelect = 0;
  //名称数据
  final List<Friends> _listDatas = [];

  @override
  void initState() {
    super.initState();
//    _listDatas.addAll(datas);
//    _listDatas.addAll(datas);
    //链式编程,等同于上面的两个
    _listDatas..addAll(datas)..addAll(datas);
    //排序!
    _listDatas.sort((Friends a, Friends b) {
      return a.indexLetter.compareTo(b.indexLetter);
    });

    var _groupOffset = 0.0;
//经过循环计算,将每一个头的位置算出来,放入字典
    for (int i = 0; i < _listDatas.length; i++) {
      if (i < 1 || _listDatas[i].indexLetter != _listDatas[i - 1].indexLetter) {//这里为分组的组头位置
        //第一个cell
        _groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
        //保存完了再加——groupOffset偏移
        _groupOffset += 84.5;
      } else {// 这里是租内的内容便宜位置
//        if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) {
        //此时没有头部,只需要加偏移量就好了
        _groupOffset += 54.5;
      }
    }
    // 监听当前页面滑动的偏移量,判断和_groupOffsetMap存储的每组组头记录的偏移量的位置,并把位置传入到指示器页面,刷新当前指示器指示的位置
    _scrollController = ScrollController()
      ..addListener(() {
        print("offset = ${_scrollController.offset}");
        for (int i = 0; i < _groupOffsetMap.values.length - 1; i++) {
          double start = _groupOffsetMap.values.elementAt(i);
          double end = _groupOffsetMap.values.elementAt(i + 1);
          print(start);
          print(end);
          if (start < _scrollController.offset &&
              end > _scrollController.offset) {
            sonKey.currentState.setIndexSelect(i);
            break;
          }
          if (start == _scrollController.offset) {
            sonKey.currentState.setIndexSelect(i);
            break;
          }
          if (end == _scrollController.offset) {
            sonKey.currentState.setIndexSelect(i + 1);
            break;
          }
        }
      });
  }
  // 每一条名称的数据
  Widget _itemForRow(BuildContext context, int index) {
    //显示剩下的cell
    //如果当前和上一个cell的indexLetter一样,就不显示
    bool _hideIndexLetter = (index > 0 &&
        _listDatas[index].indexLetter == _listDatas[index - 1].indexLetter);
    return _FriendsCell(
      imageUrl: _listDatas[index].imageUrl,
      name: _listDatas[index].name,
      groupTitle: _hideIndexLetter ? null : _listDatas[index].indexLetter,
    );
  }

  @override
  Widget build(BuildContext context) {
    return CustomScaffold(
      backgroundColor: ColorUtils.MAIN_BG,
      safeBottom: false,
      contentWidget: Flex(
        direction: Axis.vertical,
        children: <Widget>[
          TitleBar(//这个特别强调一下,这个是自定义导航类,完全可以删除
            leftTv: "",
            titleTv: "幼儿列表",
            rightTv: "",
            callBack: this,
          ),
          Expanded(
            child: Stack(
              children: <Widget>[
                Container(
                  color: ColorUtils.MAIN_BG,
                  child: ListView.builder(
                    controller: _scrollController,
                    itemCount: _listDatas.length,
                    itemBuilder: _itemForRow,
                  ),
                ), //列
                // key使用定义sonKey
                IndexBar(
                  key: sonKey,
                  indexBarCallBack: (String str) {
                    if (_groupOffsetMap[str] != null) {
                      _scrollController.animateTo(_groupOffsetMap[str],
                          duration: Duration(milliseconds: 1),
                          curve: Curves.easeIn);
                    }
                  },
                ), // 表
                //悬浮检索控件
              ],
            ),
          ),
        ],
      ),
    );
  }
  // 导航的点击事件可以删除
  @override
  void didSelectLeftCallBack() {
    Navigator.pop(context);
  }
   // 导航的点击事件可以删除
  @override
  void didSelectRightCallBack() {}
}

//这里为cell样式,就不过多介绍了
class _FriendsCell extends StatelessWidget {
  final String imageUrl;
  final String name;
  final String groupTitle;
  final String imageAssets;

  const _FriendsCell(
      {this.imageUrl, this.name, this.imageAssets, this.groupTitle}); //首字母大写
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        
      },
      child: Container(
        child: Column(
          children: <Widget>[
            Container(//
              alignment: Alignment.centerLeft,
              padding: EdgeInsets.only(left: 10),
              height: groupTitle != null ? 30 : 0,
              color: Color.fromRGBO(1, 1, 1, 0.0),
              child: groupTitle != null
                  ? Text(
                      groupTitle,
                      style: TextStyle(fontSize: 18, color: Colors.grey),
                    )
                  : null,
            ), //组头
            Container(
              color: Colors.white,
              child: Row(
                children: <Widget>[
                  Container(
                    margin: EdgeInsets.all(10),
                    width: 35,
                    height: 35,
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(6.0),
                      image: DecorationImage(
                        image: imageUrl != null
                            ? NetworkImage(imageUrl)
                            : AssetImage(imageAssets),
                      ),
                    ),
                  ), //图片
                  Expanded(
                    child: Flex(
                      direction: Axis.vertical,
                      children: <Widget>[
                        Container(
                          margin: new EdgeInsets.only(left: 10),
                          child: Text(
                            name,
                            style: TextStyle(
                              fontSize: FontSizeUtils.FONT_SIZE_15,
                              color: ColorUtils.TEXT_TITLE_BLACK,
                            ),
                          ),
                          alignment: Alignment.centerLeft,
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ), //通讯录组内容
            Container(
              height: 0.5,
              color: ColorUtils.MAIN_BG,
              child: Row(
                children: <Widget>[
                  Container(
                    width: 50,
                    color: Colors.white,
                  )
                ],
              ),
            ) //分割线
          ],
        ),
      ),
    );
  }
}

到此就介绍完了分组效果,实现的效果上面也有,如果不愿意敲也可以直接粘贴代码,把无用的东西去除就可以了,有问题随时私信我。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-07-17 12:35:42  更:2021-07-17 12:35:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/8 7:50:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码