Flutter入门(八)微信项目---通讯录页面

今天我们就来完成微信项目中的第三个页面通讯录

微信通讯录页面

首先还是老样子,我们先来分析整个UI,整个页面由3个元素所组成

  • 导航栏右侧的按钮
  • 页面右侧的导航条
  • 通讯录的列表

导航栏右侧按钮

class FriendsPage extends StatefulWidget {
  const FriendsPage({Key? key}) : super(key: key);

  @override
  State createState() => _FriendsPageState();
}

class _FriendsPageState extends State<FriendsPage> {

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: themeColor,
          title: Text('通讯录', style: TextStyle(color: Colors.black)),
          elevation: 0.0,
          actions: [
            GestureDetector(
              onTap: (){
                print('点击了添加好友按钮');
              },
              child: Container(
                margin: EdgeInsets.only(right: 10),
                child: Image(image: AssetImage('images/icon_friends_add.png'),width: 25,),
              ),
            )
          ],
        ),
        body: Container()
      ),
    );
  }
}
  • actions可以添加多个导航栏元素
  • 添加好友是一个按钮需要与用户交互,我们采用GestureDetector

通讯录的列表

  • 右侧导航条覆盖在通讯录列表上,我们采用Stack层次布局的方式
  • 通过观察我们发现每个cell都有一个图标,已经名字,有的还有索引

那我们先建立通讯路列表模型

import 'package:flutter/material.dart';

class Friend {
  final String? imageUrl; //用户头像
  final String? name; //名称
  final String? imageAssets; //4个固定cell本地图标名称
  final String? indexLetter; //索引字母

  Friend({this.imageUrl, this.name, this.indexLetter, this.imageAssets});
}

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

通讯录列表cell

import 'package:flutter/material.dart';
import 'package:wechat/Pages/const.dart';

class FriendsCell extends StatelessWidget {
  final String? imageUrl; //头像
  final String? name; //名称
  final String? imageAssets; //本地图标
  final String? groupTitle; //索引title

  const FriendsCell(
      {Key? key, this.imageUrl, this.name, this.imageAssets, this.groupTitle})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
        child: Column(
      children: [
        //头部
        Container(
          height: groupTitle != null ? 30 : 0,
          color: Color.fromRGBO(1, 1, 1, 0),
          alignment: Alignment.centerLeft,
          padding: EdgeInsets.only(left: 10),
          child: Container(
            child: groupTitle != null
                ? Text(
                    groupTitle!,
                    style: TextStyle(
                      color: Colors.grey,
                      fontSize: 15,
                    ),
                  )
                : null,
          ),
        ),
        //内容
        Container(
          color: Colors.white,
          child: Row(
            children: [
              //头像
              Container(
                height: 34,
                width: 34,
                margin: EdgeInsets.all(10),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(6),
                  image: imageUrl != null
                      ? DecorationImage(image: NetworkImage(imageUrl!))
                      : DecorationImage(
                          image: AssetImage(imageAssets!),
                        ),
                ),
              ),
              //昵称
              Expanded(
                  child: Container(
                child: Column(
                  children: [
                    //名字
                    Container(
                      height: 53,
                      alignment: Alignment.centerLeft,
                      child: Text(name!),
                    ),
                    //下划线
                    Container(
                      color: themeColor,
                      height: 0.5,
                    )
                  ],
                ),
              )),
            ],
          ),
        ),
      ],
    ));
  }
}

cell的ui在前面几篇文章中都有足够的介绍了,在这里就不做过多介绍。
⚠️注意:这里我们把索引也看作是cell的一部分,当有传groupTitle时才会显示索引条,没有传则隐藏

通讯录整个列表

class FriendsPage extends StatefulWidget {
  const FriendsPage({Key? key}) : super(key: key);

  @override
  State createState() => _FriendsPageState();
}

class _FriendsPageState extends State<FriendsPage> {

  final List<Friend> _headerData = [
    Friend(imageAssets: 'images/icon_new_friends.png', name: '新的朋友'),
    Friend(imageAssets: 'images/icon_group.png', name: '群聊'),
    Friend(imageAssets: 'images/icon_label.png', name: '标签'),
    Friend(imageAssets: 'images/icon_accounts.png', name: '公众号'),
  ];

  final List<Friend> _listDatas = [];

  // ignore: must_call_super
  @override void initState() {
    super.initState();
    _listDatas.addAll(datas);
    _listDatas.sort((Friend A, Friend B) {
      return A.indexLetter!.compareTo(B.indexLetter!);
    });
  }

  Widget _itemForRow(BuildContext context, int index) {
    if (index < _headerData.length) {
      return FriendsCell(name: _headerData[index].name, imageAssets: _headerData[index].imageAssets,);
    }
    int _currentIndex = index - _headerData.length;
    String? _groupTitle = _listDatas[_currentIndex].indexLetter;
    if (_currentIndex > 0) {
      if (_groupTitle == _listDatas[_currentIndex-1].indexLetter){
        _groupTitle = null;
      }
    }
    return FriendsCell(name: _listDatas[_currentIndex].name, imageUrl: _listDatas[_currentIndex].imageUrl, groupTitle: _groupTitle,);
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: themeColor,
          title: Text('通讯录', style: TextStyle(color: Colors.black)),
          elevation: 0.0,
          actions: [
            GestureDetector(
              onTap: (){
                print('点击了添加好友按钮');
              },
              child: Container(
                margin: EdgeInsets.only(right: 10),
                child: Image(image: AssetImage('images/icon_friends_add.png'),width: 25,),
              ),
            )
          ],
        ),
        body: Stack(
          children: [
            //列表
            Container(
              color: themeColor,
              child: ListView.builder(
                itemBuilder: _itemForRow,
                itemCount:_listDatas.length + _headerData.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
  • _headerData头部4个固定cell的数据
  • _listDatas除头部4个固定cell外的列表数据
  • initState页面初始化调用,这里对数据的处理
  • _itemForRowcell的处理

到这里我们已经完成了除右侧索引条外所有的UI布局了,我们来看下效果


索引条构建
索引条分为条部分:1右侧的字母栏,2左侧的选中指示

import 'package:flutter/material.dart';
import 'package:wechat/Pages/const.dart';

class IndexBar extends StatefulWidget {
  final void Function(String str) indexBarCallBack;
  const IndexBar({Key? key, required this.indexBarCallBack}) : super(key: key);

  @override
  _IndexBarState createState() => _IndexBarState();
}

int getIndex(BuildContext context, Offset globalPosition) {
  //拿到Box
  RenderBox box = context.findRenderObject() as RenderBox;
  //拿到y值
  double y = box.globalToLocal(globalPosition).dy;
  //算出字符高度
  var itemHeight = screenHeight(context) / 2 / indexWords.length;
  //算出第几个item ~/ 除法取整. 并且给一个取值范围
  int index = (y ~/ itemHeight).clamp(0, indexWords.length - 1);
  return index;
}

class _IndexBarState extends State<IndexBar> {
  Color _bkColor = const Color.fromRGBO(1, 1, 1, 0.0);
  Color _textColor = Colors.black;
  double _indicatorY = 0.0;
  String _indicatorText = 'A';
  bool _indicatorHidden = true;

  @override
  Widget build(BuildContext context) {
    //悬浮菜单
    final List<Widget> words = [];
    for (int i = 0; i < indexWords.length; i++) {
      words.add(
        Expanded(
          child: Text(
            indexWords[i],
            style: TextStyle(fontSize: 10, color: _textColor),
          ),
        ),
      );
    }
    return Positioned(
      width: 120,
      right: 0,
      height: screenHeight(context)/2,
      top: screenHeight(context)/8,
      child: Row(
        children: [
          //指示器
          Container(
            width: 90,
            alignment: Alignment(0, _indicatorY),
            child: _indicatorHidden ? null : Stack(
              alignment: Alignment(-0.2, 0),
              children: [
                //背景
                const Image(
                  image: AssetImage('images/icon_bubble.png'),
                  width: 60,
                ),
                //文字
                Text(
                  _indicatorText,
                  style: const TextStyle(
                    fontSize: 35,
                    color: Colors.white,
                  ),
                ),
              ],
            ),
          ),
          //索引条
          GestureDetector(
            onVerticalDragDown: (DragDownDetails details) {
              int index = getIndex(context, details.globalPosition);
              widget.indexBarCallBack(indexWords[index]);
              setState(() {
                _bkColor = Color.fromRGBO(1, 1, 1, 0.5);
                _textColor = Colors.white;
                _indicatorY = 2.2/ 28 * index - 1.1;
                _indicatorText = indexWords[index];
                _indicatorHidden = false;
              });
            },
            onVerticalDragEnd: (DragEndDetails details) {
              setState(() {
                _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
                _textColor = Colors.black;
                _indicatorHidden = true;
              });
            },
            onVerticalDragUpdate: (DragUpdateDetails details){
              int index = getIndex(context, details.globalPosition);
              widget.indexBarCallBack(indexWords[index]);
              setState(() {
                _indicatorText = indexWords[index];
                _indicatorY = 2.2/ 28 * index - 1.1;
                _indicatorHidden = false;
              });
            },
            child: Container(
              width: 30,
              color: _bkColor,
              child: Column(
                children: words,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

const indexWords = [
  '🔍',
  '☆',
  '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'
];

通讯录页的完整代码

import 'package:flutter/material.dart';
import 'package:wechat/Pages/const.dart';
import 'package:wechat/Pages/friends/friends_cell.dart';
import 'package:wechat/Pages/friends/friends_datas.dart';
import 'friends_index_bar.dart';

class FriendsPage extends StatefulWidget {
  const FriendsPage({Key? key}) : super(key: key);

  @override
  State createState() => _FriendsPageState();
}

class _FriendsPageState extends State<FriendsPage> {
  //创建字典,里面放item和高度的对应数据!
  final Map _groupOffsetMap = {
    indexWords[0]: 0.0,
    indexWords[1]: 0.0,
  };

  late final ScrollController _scrollController = ScrollController();

  final List<Friend> _headerData = [
    Friend(imageAssets: 'images/icon_new_friends.png', name: '新的朋友'),
    Friend(imageAssets: 'images/icon_group.png', name: '群聊'),
    Friend(imageAssets: 'images/icon_label.png', name: '标签'),
    Friend(imageAssets: 'images/icon_accounts.png', name: '公众号'),
  ];

  final List<Friend> _listDatas = [];

  // ignore: must_call_super
  @override
  void initState() {
    super.initState();
    _listDatas.addAll(datas);
    _listDatas.sort((Friend A, Friend B) {
      return A.indexLetter!.compareTo(B.indexLetter!);
    });

    //通过循环计算,将每一个头的位置算出来,放入字典!
    double cellHeight = 54.0;
    double groupHeight = 30.0;
    double groupOffset = cellHeight * 4; //第一个Cell的位置,4个Cell的高度!
    for (int i = 0; i < _listDatas.length; i++) {
      String? _groupT = _listDatas[i].indexLetter;
      if (i == 0) {
        _groupOffsetMap.addAll({_listDatas[i].indexLetter: groupOffset});
      } else {
        if (_groupT != _listDatas[i - 1].indexLetter) {
          groupOffset = groupHeight + groupOffset;
          _groupOffsetMap.addAll({_listDatas[i].indexLetter: groupOffset});
        }
      }
      groupOffset = groupOffset + cellHeight;
    }
  }

  Widget _itemForRow(BuildContext context, int index) {
    if (index < _headerData.length) {
      return FriendsCell(
        name: _headerData[index].name,
        imageAssets: _headerData[index].imageAssets,
      );
    }
    int _currentIndex = index - _headerData.length;
    String? _groupTitle = _listDatas[_currentIndex].indexLetter;
    if (_currentIndex > 0) {
      if (_groupTitle == _listDatas[_currentIndex - 1].indexLetter) {
        _groupTitle = null;
      }
    }
    return FriendsCell(
      name: _listDatas[_currentIndex].name,
      imageUrl: _listDatas[_currentIndex].imageUrl,
      groupTitle: _groupTitle,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: themeColor,
          title: Text('通讯录', style: TextStyle(color: Colors.black)),
          elevation: 0.0,
          actions: [
            GestureDetector(
              onTap: () {
                print('点击了添加好友按钮');
              },
              child: Container(
                margin: EdgeInsets.only(right: 10),
                child: Image(
                  image: AssetImage('images/icon_friends_add.png'),
                  width: 25,
                ),
              ),
            )
          ],
        ),
        body: Stack(
          children: [
            //列表
            Container(
              color: themeColor,
              child: ListView.builder(
                controller: _scrollController,
                itemBuilder: _itemForRow,
                itemCount: _listDatas.length + _headerData.length,
              ),
            ),
            //检索栏
            IndexBar(
              indexBarCallBack: (String str) {
                if (_groupOffsetMap[str] != null) {
                  _scrollController.animateTo(_groupOffsetMap[str],
                      duration: Duration(milliseconds: 10),
                      curve: Curves.easeIn);
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

到这里我们的通讯录页面就已经完成,我们来看下最后的效果

通讯录最终效果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,764评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,235评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 171,965评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,984评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,984评论 6 399
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,471评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,844评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,818评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,359评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,385评论 3 346
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,515评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,114评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,836评论 3 338
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,291评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,422评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,064评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,581评论 2 365

推荐阅读更多精彩内容