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  {
  @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  {
  @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  {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: _indexes(),
    );
  }

  List  _indexes(){
    List  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):我的页面上拉下拉动画
下一篇:未完待续

你可能感兴趣的:(Flutter 仿生微信(6):联系人页面列表展示)