微信项目 - 通讯录与索引条

前面我们开发了微信项目的发现页面与我的页面,下面我们来开发通讯录页面

通讯录导航栏

  • 新建friends目录,把friends_page.dart页面放入;再创建数据文件friends_data.dart
// friends_data.dart文件
class Friends {
  Friends({this.imageUrl, this.name, this.indexLetter});
  final String imageUrl;
  final String name;
  final String 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'),
];
  • 为了修改AppBar背景色,我们之前是在每个页面配置主题色;其实我们可以新建文件来配置全局常量
// 之前配置背景色方式
class _FriendsPageState extends State<FriendsPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // 配置AppBar背景色
        backgroundColor: Color.fromRGBO(220, 220, 220, 1.0),
        title: const Text('通讯录'),
      ),
......
  • 新建文件const.dart来配置全局常量
import 'package:flutter/material.dart';

//主题色
//const修饰的常量,名称首字母可以是大写
const Color WeChatThemeColor = Color.fromRGBO(220, 220, 220, 1.0);

//屏幕宽度
//屏幕宽度是通过计算来获取,不能定义成常量;首字母需要小写
double screenWidth(BuildContext context) => MediaQuery.of(context).size.width;
double screenHeigth(BuildContext context) => MediaQuery.of(context).size.height;
  • 使用全局常量来配置AppBar背景色
// 导入头文件
import '../const.dart';

class _FriendsPageState extends State<FriendsPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: WeChatThemeColor,
        title: const Text('通讯录'),
      ),
......
  • 通讯录页面添加右上角添加好友按钮
class _FriendsPageState extends State<FriendsPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // 右上角添加好友按钮
        actions: [
          GestureDetector(
            onTap: (){
              // 点击跳转添加好友页面
              Navigator.of(context).push(MaterialPageRoute(
                  builder: (BuildContext context) => DiscoverChildPage(
                    title: '添加朋友',
                  )));
            },
            child: Container(
                margin: EdgeInsets.only(right: 10),
                child: Image(
                  image: AssetImage('images/icon_friends_add.png'),
                  width: 25,
                ),
              )
          )
        ],
        backgroundColor: WeChatThemeColor,
        title: const Text('通讯录'),
      ),
      body: const Center(
        child: Text('通讯录'),
      ),
    );
  }
}
运行效果

通讯录列表

Flutter界面是否显示一般两种判断方法

  1. 界面内部判断,根据成员是否有值 -> 是否显示
  2. 创建cell的时候,通过参数判断
  • 创建通讯录列表Cell,由于通讯录前四条数据是固定的,需要本地图片资源属性imageAssets
class _FriendCell extends StatelessWidget {
  _FriendCell(
      {this.imageUrl,
       this.name,
       this.groupTitle,
       this.imageAssets});
  final String imageUrl;
  final String name;
  final String groupTitle;
  final String imageAssets;
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: Row(
        children: [
          Container(
            margin: EdgeInsets.all(10),
            width: 34,
            height: 34,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              // 装饰器image
              image: DecorationImage(
                image: imageUrl != null
                    ? NetworkImage(imageUrl)
                    : AssetImage(imageAssets),
              ),
            ),
          ), //图片
          Container(
            width: screenWidth(context) - 54,
            child: Column(
              children: [
                Container(
                  alignment: Alignment.centerLeft,
                  height: 54,
                  child: Text(
                    name,
                    style: TextStyle(fontSize: 18),
                  ),
                ),
                // Cell下划线
                Container(
                  height: 0.5,
                  color: WeChatThemeColor,
                )
              ],
            ),
          ), //昵称+分割线
        ],
      ),
    );
  }
}
  • Friends模型中添加获取本地资源的属性imageAssets
class Friends {
  Friends({
    this.imageAssets,
    this.imageUrl,
    this.name,
    this.indexLetter
  });
  final String imageAssets;
  final String imageUrl;
  final String name;
  final String indexLetter;
}
  • 通讯录列表ListView
class _FriendsPageState extends State<FriendsPage> {
  // 前四条固定数据
  final List<Friends> _headerData = [
    Friends(imageAssets: 'images/新的朋友.png', name: '新的朋友'),
    Friends(imageAssets: 'images/群聊.png', name: '群聊'),
    Friends(imageAssets: 'images/标签.png', name: '标签'),
    Friends(imageAssets: 'images/公众号.png', name: '公众号'),
  ];

  Widget _itemForRow(BuildContext context, int index) {
    //显示头部4个Cell
    if(index < _headerData.length) {
      return _FriendCell(
        imageAssets: _headerData[index].imageAssets,
        name: _headerData[index].name);
    }
    // 由于列表前四条数据是固定的,数据源需要减去4才能从零开始显示
    return _FriendCell(imageUrl: datas[index - 4].imageUrl,
                       name: datas[index - 4].name);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          GestureDetector(
            onTap: (){
              Navigator.of(context).push(MaterialPageRoute(
                  builder: (BuildContext context) => DiscoverChildPage(
                    title: '添加朋友',
                  )));
            },
            child: Container(
                margin: EdgeInsets.only(right: 10),
                child: Image(
                  image: AssetImage('images/icon_friends_add.png'),
                  width: 25,
                ),
              )
          )
        ],
        backgroundColor: WeChatThemeColor,
        title: const Text('通讯录'),
      ),
      body: Container(
        color: WeChatThemeColor,
        child: ListView.builder(
            itemBuilder: _itemForRow,
            itemCount: datas.length + _headerData.length,
        ),
      ),
    );
  }
}
运行效果

显示分组cell的头

  • Cell添加分组头,Alignment.centerLeft属性让分组头内容居左
class _FriendCell extends StatelessWidget {
  _FriendCell(
      { this.imageUrl,
        this.name,
        this.groupTitle,
        this.imageAssets});
  final String imageUrl;
  final String name;
  final String groupTitle;
  final String imageAssets;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [

        // 分组头部
        Container(
          // 分组内容居左
          alignment: Alignment.centerLeft,
          padding: EdgeInsets.only(left: 10),
          height: groupTitle != null ? 30 : 0,
          color: WeChatThemeColor,
          child: groupTitle != null
              ? Text(
                  groupTitle,
                  style: TextStyle(color: Colors.grey),
                )
              : null,
        ),

        Container(
          color: Colors.white,
          child: Row(
            children: [
              Container(
                margin: EdgeInsets.all(10),
                width: 34,
                height: 34,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(6.0),
                  // 装饰器image
                  image: DecorationImage(
                    image: imageUrl != null
                        ? NetworkImage(imageUrl)
                        : AssetImage(imageAssets),
                  ),
                ),
              ), //图片
              Container(
                width: screenWidth(context) - 54,
                child: Column(
                  children: [
                    Container(
                      alignment: Alignment.centerLeft,
                      height: 54,
                      child: Text(
                        name,
                        style: TextStyle(fontSize: 18),
                      ),
                    ),
                    Container(
                      height: 0.5,
                      color: WeChatThemeColor,
                    )
                  ],
                ),
              ), //昵称+分割线
            ],
          ),
       ),
      ],
    );
  }
}
  • 构造页面数据,initState方法只有在_FriendsPageState页面载入的时候才会执行,热重载是不会执行的。
    切换Tab,flutter页面会销毁,重新进入通讯录页面相当于重新载入,会执行initState方法。
class _FriendsPageState extends State<FriendsPage> {

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

  final List<Friends> _listDatas = [];

  @override
  // _FriendsPageState页面载入的时候会执行,热重载是不会执行的
  void initState() {
    // TODO: implement initState
    super.initState();
    // 创建数据, 链式表达
    _listDatas..addAll(datas)..addAll(datas); // 等价于 _listDatas.addAll(datas);_listDatas.addAll(datas);
    // 数据排序
    _listDatas.sort((Friends a, Friends b) {
      return a.indexLetter.compareTo(b.indexLetter);
    });
  }
......
  • _itemForRow方法调用修改
 Widget _itemForRow(BuildContext context, int index) {
    //显示头部4个Cell
    if(index < _headerData.length) {
      return _FriendCell(
        imageAssets: _headerData[index].imageAssets,
        name: _headerData[index].name);
    }
    // 剩下的Cell
    // 是否显示组名字
    bool _hiddenIndexLetter = (index - 4 > 0 &&
        _listDatas[index - 4].indexLetter == _listDatas[index - 5].indexLetter);
    //显示头
    return _FriendCell(
      imageUrl: _listDatas[index - 4].imageUrl,
      name: _listDatas[index - 4].name,
      // 分组头部数据
      groupTitle: _hiddenIndexLetter ? null : _listDatas[index - 4].indexLetter,
    );
  }

// 注意⚠️ListView -> itemCount使用_listDatas数据
body: Container(
        color: WeChatThemeColor,
        child: ListView.builder(
            itemBuilder: _itemForRow,
            itemCount: _listDatas.length + _headerData.length,
        ),
      ),
运行效果

显示索引条

  • friends_data.dart文件中添加右侧索引条数据
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'
];
  • 构造索引条数据
class _FriendsPageState extends State<FriendsPage> {
  final List<Widget> words = [];
......  
@override
  // _FriendsPageState页面载入的时候会执行,热重载是不会执行的
  void initState() {
    // TODO: implement initState
    super.initState();
    // 创建数据, 链式表达
    _listDatas..addAll(datas)..addAll(datas); // 等价于 _listDatas.addAll(datas);_listDatas.addAll(datas);
    // 数据排序
    _listDatas.sort((Friends a, Friends b) {
      return a.indexLetter.compareTo(b.indexLetter);
    });
    // 索引条数据
    for (int i = 0; i < INDEX_WORDS.length; i++) {
      words.add(Expanded(child: Text(
          INDEX_WORDS[i],
          style: TextStyle(fontSize: 10, color: Colors.grey),
      )));
    }
  }
......
  • 页面添加悬浮索引条
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          GestureDetector(
            onTap: (){
              Navigator.of(context).push(MaterialPageRoute(
                  builder: (BuildContext context) => DiscoverChildPage(
                    title: '添加朋友',
                  )));
            },
            child: Container(
                margin: EdgeInsets.only(right: 10),
                child: Image(
                  image: AssetImage('images/icon_friends_add.png'),
                  width: 25,
                ),
              )
          )
        ],
        backgroundColor: WeChatThemeColor,
        title: const Text('通讯录'),
      ),
      body: Stack(
        children: [
          Container(
            color: WeChatThemeColor,
            child: ListView.builder(
              itemBuilder: _itemForRow,
              itemCount: _listDatas.length + _headerData.length,
            ),
          ),
          // 悬浮索引条
          Positioned(
            right: 0.0,
            top: screenHeigth(context) / 8,
            height: screenHeigth(context) / 2,
            width: 30,
            child: Column(
              children: words,
            ),
          ),
        ],
      ),
    );
  }
运行效果

抽取索引条

  • 新建index_bar.dart文件,抽取索引条
  1. words以及循环遍历数据,抽取到index_bar.dart文件
import 'package:flutter/material.dart';
import '../const.dart';
import 'friends_data.dart';

class IndexBar extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _IndexBarState();
}

class _IndexBarState extends State<IndexBar> {

  final List<Widget> words = [];

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    // 右侧索引数据
    for (int i = 0; i < INDEX_WORDS.length; i++) {
      words.add(Expanded(child: Text(
        INDEX_WORDS[i],
        style: TextStyle(fontSize: 10, color: Colors.grey),
      )));
    }
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Positioned(
      right: 0.0,
      top: screenHeigth(context) / 8,
      height: screenHeigth(context) / 2,
      width: 30,
      child: Column(
        children: words,
      ),
    );
  }
}
  • friends_page.dart文件使用IndexBar
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          GestureDetector(
            onTap: (){
              Navigator.of(context).push(MaterialPageRoute(
                  builder: (BuildContext context) => DiscoverChildPage(
                    title: '添加朋友',
                  )));
            },
            child: Container(
                margin: EdgeInsets.only(right: 10),
                child: Image(
                  image: AssetImage('images/icon_friends_add.png'),
                  width: 25,
                ),
              )
          )
        ],
        backgroundColor: WeChatThemeColor,
        title: const Text('通讯录'),
      ),
      body: Stack(
        children: [
          Container(
            color: WeChatThemeColor,
            child: ListView.builder(
              itemBuilder: _itemForRow,
              itemCount: _listDatas.length + _headerData.length,
            ),
          ),
          // 悬浮索引条
          IndexBar()
        ],
      ),
    );
  }
运行效果

选中索引条

import 'package:flutter/material.dart';
import '../const.dart';
import 'friends_data.dart';

class IndexBar extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _IndexBarState();
}

// 获取选中的item的字符!!
String getIndex(BuildContext context, Offset globalPosition) {
  // 获取当前小部件的盒子
  RenderBox box = context.findRenderObject();
  // 获取y值 globalToLocald当前位置距离部件原点(左上角)的位置
  double y = box.globalToLocal(globalPosition).dy;
  // 算出字符高度
  var itemHeight = screenHeigth(context) / 2 / INDEX_WORDS.length;
  // 算出第几个item, clamp约束index范围值
  int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
  return INDEX_WORDS[index];
}

class _IndexBarState extends State<IndexBar> {

  // 索引条选中 背景色与文字颜色
  Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
  Color _textColor = Colors.black;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // 与界面相关的数据要放入build中
    final List<Widget> words = [];
    for (int i = 0; i < INDEX_WORDS.length; i++) {
      words.add(Expanded(child: Text(
        INDEX_WORDS[i],
        style: TextStyle(fontSize: 10, color: _textColor),
      )));
    }

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

推荐阅读更多精彩内容