今天我们就来完成微信项目中的第三个页面通讯录
微信通讯录
页面
首先还是老样子,我们先来分析整个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
页面初始化调用,这里对数据的处理 -
_itemForRow
对cell
的处理
到这里我们已经完成了除右侧索引条外所有的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);
}
},
),
],
),
),
);
}
}
到这里我们的通讯录
页面就已经完成,我们来看下最后的效果
通讯录最终效果