Flutter 仿生微信(6):联系人页面列表展示

1. 源码下载

喜欢的话,别忘了点个关注,还有给个 Github 右上角的小星星吧。

源码下载地址,代码会根据不断更新。

Package 安装教程,找了好久,没有合适的轮子可以用,就自己写了个轮子上传到 pub.dev 了,回头单开一篇来讲一下 package 的制作与上传。

目录
上一篇:Flutter 仿生微信(5):我的页面上拉下拉动画
下一篇:未完待续
FMWeixin Contacts.gif

2. 思路

列表页整体分为两个部分,联系人列表以及右侧索引。

  • 整体布局

使用 Stack 布局,增加 FMContactContent 和 FMContactsIndexes 作为 children 布局,然后根据当前返回的 section 做联动处理。

  • package 制作思路

考虑以后这里肯定要动态化定制,我们尽可能的将数据层留给外部处理,所以写 package 的时候要考虑低耦合。目前 ui_tableview 这个 package 几乎将所有定制化的东西都通过回调函数抛到外部页面,后续需要的新功能会继续增加,也欢迎大家的建议,希望能写出一个易用、适用更多场景的轮子。

Name Description Required Default value
numberOfSections final int Function(BuildContext context) numberOfSections 外部返回几组 - -
numberOfRowsInSection int Function(BuildContext context, int section) numberOfRowsInSection 外部返回每组生成几行item required -
widgetForHeaderInSection Widget Function(BuildContext context, int section) widgetForHeaderInSection 外部返回每组对应的 Header - -
itemForRowAtIndexPath Widget Function(BuildContext context, UIIndexPath indexPath) itemForRowAtIndexPath 外部返回对应IndexPath(section, row)的 widget required -
heightForRowAtIndexPath double Function(BuildContext context, UIIndexPath indexPath) heightForRowAtIndexPath 外部返回对应IndexPath(section, row)的 widget 的行高 - 默认 44
heightForHeaderInSection double Function(BuildContext context, int section) heightForHeaderInSection 外部返回对应 section 的 Header 高度 - 默认 30
needHover 上方是否需要悬停条 - 默认为 true
widgetForHoverHeader Widget Function(BuildContext context, int section) widgetForHoverHeader 外部返回当前 section 对应的悬停条,可根据section自定义不同样式。当 widgetForHoverHeader == null时,会执行 widgetForHeaderInSection 回调来获取悬停条 - -
heightForHoverHeaderInSection double Function(BuildContext context, int section) heightForHoverHeaderInSection 外部返回当前section对应的悬停条高度。当 heightForHoverHeaderInSection == null 时,执行 heightForHeaderInSection 来获取高度 - 默认 30
scrollViewDidScroll double Function(double offset) scrollViewDidScroll,设置 scrollViewDidScroll 后,当页面滚动时,这里会返回对应 offset - -
tableViewDidChangeSection int Function(int section) tableViewDidChangeSection 处于最上方的section发生改变时回调 - -
contentOffset 设置初始偏移量 - -
  • 静态功能搭建

package 写好后,轮子就好写了,这里我们只需要根据提供的 api 来控制页面展示。

首先我们按照准备的 sections (星标,A....Z) 等,总数 +1,来配置 numberOfSections 数量。当 section ==0 时,我们配置静态功能(新的朋友、群聊、标签、公众号)等功能。

  • 动态数据搭建

由于目前没有引用数据,就先不建立 model 了,后续根据需要在进行优化。itemForRowAtIndexPath 这个 api 会返回一个 indexPath 属性,indexPath 一共包含了2个参数,indexPath.section,indexPath.row。

indexPath 明确的反馈了,当前返回的 item 是放在第几组第几个的。我们可以根据这个,做一些动态化的区分,比如不规则列表,配合 heightForRowAtIndexPath 实现不同高度。

3. 示例代码

FMContacts.dart

import 'package:FMWeixinApp/contacts/contacts/content/FMContactContent.dart';
import 'package:FMWeixinApp/contacts/contacts/index/FMContactIndexes.dart';
import 'package:flutter/material.dart';

class FMMailList extends StatefulWidget {
  @override
  FMMailListState createState()=> FMMailListState();
}

class FMMailListState extends State <FMMailList> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text('通讯录'),
        elevation: 1.0,
      ),
      body: _stack(),
    );
  }

  Stack _stack(){
    return Stack(
      children: [
        FMContactContent(),
        Positioned(
          top: 100,
          bottom: 100,
          width: 30,
          right: 15,
          child: FMContactsIndexes(),
        ),
      ],
    );
  }
}

FMContactContent.dart

import 'package:FMWeixinApp/tools/FMColor.dart';
import 'package:flutter/material.dart';
import 'package:ui_tableview/ui_tableview.dart';

const List _sections = ['星标', '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 FMContactContent extends StatefulWidget {
  @override
  FMContactContentState createState() => FMContactContentState();
}

class FMContactContentState extends State <FMContactContent> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build

    UITableView _contacts(){
      int _numberOfRowsInSection(context, index){
        return 4;
      }

      int _numberOfSections(context){
        return _sections.length + 1;
      }

      Widget _itemForRowAtIndexPath(context, UIIndexPath indexPath){
        String text;
        Image image;

        if (indexPath.section == 0) {
          if (indexPath.row == 0) {
            text = '新的朋友';
            image = Image.asset('assets/images/contacts/contacts_new_friend.png');
          } else if (indexPath.row == 1) {
            text = '群聊';
            image = Image.asset('assets/images/contacts/contacts_grouped.png');
          } else if (indexPath.row == 2) {
            text = '标签';
            image = Image.asset('assets/images/contacts/contacts_tag.png');
          } else if (indexPath.row == 3) {
            text = '公众号';
            image = Image.asset('assets/images/contacts/contacts_public.png');
          }

          print(image);

        } else {
          text = "小小  ${_sections[indexPath.section - 1]} - ${indexPath.row}";
          image = Image.network('https://gss0.bdstatic.com/6LZ1dD3d1sgCo2Kml5_Y_D3/sys/portrait/item/tb.1.2c0e048.VFUJtjrbQl4EwWKNr0H0GA?t=1497799370');
        }

        return Stack(
          alignment: Alignment.center,
          children: [
            Container(color: Colors.white,),
            Row(
              children: [
                Padding(padding: EdgeInsets.only(left: 20)),
                Container(
                  padding: EdgeInsets.all(5),
                  child: image,
                ),
                Padding(padding: EdgeInsets.only(left: 10)),
                Text( text, style: TextStyle( fontSize: 18, ), )
              ],
            ),
            Positioned(child: Divider(height: 2,),bottom: 0, left: 80,right: 0,),
          ],
        );
      }

      Widget _widgetForHeaderInSection(context, section){
        if (section == 0) return Container();

        return Container(
          alignment: Alignment.centerLeft,
          color: FMColors.wx_gray,
          padding: EdgeInsets.only(left: 20),
          child: Text("${_sections[section - 1]}"),
        );
      }

      Widget _widgetForHoverHeader(context, section){
        if (section == 0) return Container();

        return Container(
          alignment: Alignment.centerLeft,
          color: FMColors.wx_gray,
          padding: EdgeInsets.only(left: 20),
          child: Text("${_sections[section - 1]}", style: TextStyle(color: FMColors.wx_green),),
        );
      }

      double _heightForHeaderInSection(context, section){
        if (section == 0) return 0;
        return 30;
      }

      double _heightForRowAtIndexPath(context, indexPath){
        return 55;
      }

      return UITableView(
        numberOfRowsInSection: _numberOfRowsInSection,
        numberOfSections: _numberOfSections,
        itemForRowAtIndexPath: _itemForRowAtIndexPath,
        widgetForHeaderInSection: _widgetForHeaderInSection,
        heightForRowAtIndexPath: _heightForRowAtIndexPath,

        heightForHeaderInSection: _heightForHeaderInSection,
        widgetForHoverHeader: _widgetForHoverHeader,
      );
    }
    
    return _contacts();
  }
}

FMContactsIndexes.dart

import 'package:flutter/material.dart';

const List _sections = ['星标', '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 FMContactsIndexes extends StatefulWidget {
  @override
  FMContactsIndexesState createState() => FMContactsIndexesState();
}

class FMContactsIndexesState extends State <FMContactsIndexes> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: _indexes(),
    );
  }

  List <Widget> _indexes(){
    List <Widget> widgests = [];

    _sections.forEach((title) {
      widgests.add(Text('$title'));
    });

    return widgests;
  }
}
FMWeixin Contacts.gif

4. 源码分析

这一篇其实只是简单的使用了 ui_tableview 这个 package。

4.1 返回组列个数控制

      int _numberOfRowsInSection(context, index){
        return 4;
      }

      int _numberOfSections(context){
        return _sections.length + 1;
      }

这里只是没有数据做个简单页面搭建,我们每一组都返回4行。返回组数根据 _sections.length + 1,预留出第一组用来展示静态功能。

4.2 对应 IndexPath 的 Widget 控制

      double _heightForRowAtIndexPath(context, indexPath){
        return 55;
      }

      Widget _itemForRowAtIndexPath(context, UIIndexPath indexPath){
        String text;
        Image image;

        if (indexPath.section == 0) {
          if (indexPath.row == 0) {
            text = '新的朋友';
            image = Image.asset('assets/images/contacts/contacts_new_friend.png');
          } else if (indexPath.row == 1) {
            text = '群聊';
            image = Image.asset('assets/images/contacts/contacts_grouped.png');
          } else if (indexPath.row == 2) {
            text = '标签';
            image = Image.asset('assets/images/contacts/contacts_tag.png');
          } else if (indexPath.row == 3) {
            text = '公众号';
            image = Image.asset('assets/images/contacts/contacts_public.png');
          }

          print(image);

        } else {
          text = "小小  ${_sections[indexPath.section - 1]} - ${indexPath.row}";
          image = Image.network('https://gss0.bdstatic.com/6LZ1dD3d1sgCo2Kml5_Y_D3/sys/portrait/item/tb.1.2c0e048.VFUJtjrbQl4EwWKNr0H0GA?t=1497799370');
        }

        return Stack(
          alignment: Alignment.center,
          children: [
            Container(color: Colors.white,),
            Row(
              children: [
                Padding(padding: EdgeInsets.only(left: 20)),
                Container(
                  padding: EdgeInsets.all(5),
                  child: image,
                ),
                Padding(padding: EdgeInsets.only(left: 10)),
                Text( text, style: TextStyle( fontSize: 18, ), )
              ],
            ),
            Positioned(child: Divider(height: 2,),bottom: 0, left: 80,right: 0,),
          ],
        );
      }

根据 indexPath 设置不同行高,我们这里统一设置 55。

根据 indexPath 设置不同 Widget,indexPath.section == 0 时,根据 indexPath.row 进行 新的朋友,群聊,标签,公众号的排版。

4.3 Section Header

这个就是每一组顶部的那个条条。

      double _heightForHeaderInSection(context, section){
        if (section == 0) return 0;
        return 30;
      }

      Widget _widgetForHeaderInSection(context, section){
        if (section == 0) return Container();

        return Container(
          alignment: Alignment.centerLeft,
          color: FMColors.wx_gray,
          padding: EdgeInsets.only(left: 20),
          child: Text("${_sections[section - 1]}"),
        );
      }

根据 section 的值来给每一组做定制化高度处理,当 section == 0 时,是静态功能组,没有 header,我们高度返回 0,其他组返回 30 高度。

根据 section 的值来给Header做定制化处理,当 section == 0时,返回空 Header,其他组按照微信的分组UI返回对应的 widget。

4.4 悬浮标签

这个可能是我写 ui_tableview 这个 package 的主要因素了,我找了好几个轮子,都没有微信那个上滑后,悬浮条会变成绿色的功能。

      Widget _widgetForHoverHeader(context, section){
        if (section == 0) return Container();

        return Container(
          alignment: Alignment.centerLeft,
          color: FMColors.wx_gray,
          padding: EdgeInsets.only(left: 20),
          child: Text("${_sections[section - 1]}", style: TextStyle(color: FMColors.wx_green),),
        );
      }

根据不同的 section 返回不同的悬浮条,不设置悬浮条高度时,默认和 heightForHeaderInSection 一样。

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

推荐阅读更多精彩内容