Flutter 仿生微信(6):联系人页面列表展示
1. 源码下载
喜欢的话,别忘了点个关注,还有给个 Github 右上角的小星星吧。
源码下载地址,代码会根据不断更新。
Package 安装教程,找了好久,没有合适的轮子可以用,就自己写了个轮子上传到 pub.dev 了,回头单开一篇来讲一下 package 的制作与上传。
目录
上一篇:Flutter 仿生微信(5):我的页面上拉下拉动画
下一篇:未完待续
FMWeixin Contacts.gif2. 思路
列表页整体分为两个部分,联系人列表以及右侧索引。
- 整体布局
使用 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 一样。