前语
这篇文章功用性略微多一些,首要介绍了联系人列表
、索引功用
联系人列表选用非SectionList
的方法巧妙完成,毕竟体系没提供,另外介绍右侧索引条,且确保能和左边联系人构成互动
源码地址
ps
: 能够依据需求自己封装一个 SectionList
如下图所示,首要分了四个要害单,联系人列表默许item、联系人列表、索引、导航增加老友
联系人功用,首要封装了两个控件,ContactIndexs
为索引功用,ContactCell
为列表的 item
,且了处理Section问题
导航增加老友
AppBar
的左边定制特点为leading
,设置单个组件,而右侧为 actions
,可设置多个组件,这儿简略在重复一下导航跳转,如下所示
Scaffold(
appBar: AppBar(
title: const Text("联系人"),
foregroundColor: Colors.black,
backgroundColor: const Color.fromRGBO(0xe1, 0xe1, 0xe1, 1),
elevation: 0, //去掉暗影
actions: [
TextButton(
onPressed: () {
//跳转到下一个页面
Navigator.of(context).push(MaterialPageRoute(builder:
(BuildContext context) => const AddFriend(title: "增加朋友")));
//假如要自动跳转回来
//Navigator.pop(context);
},
child: Image.asset(
"images/增加朋友.png",
width: 28,
height: 28,
),
),
],
),
//这是是联系人列表实践功用
body: Stack(
children: [
ListView.builder(
//这个controller用于 ListView滚动到指定方位
controller: _controller,
itemBuilder: itemForRow,
itemCount: headerItems.length + friends.length,
),
],
),
);
联系人列表
联系人列表首要包含列表固定头部
、动态内容
,索引
后面介绍
固定头部
:这儿先生成数据,运用动态内容的 item 数据结构,只不过只有默许的图片和姓名参数
headerItems = [
Friends(imageUrl: 'images/新的朋友.png', name: "新的朋友"),
Friends(imageUrl: 'images/群聊.png', name: "群聊"),
Friends(imageUrl: 'images/标签.png', name: "标签"),
Friends(imageUrl: 'images/公众号.png', name: "公众号"),
];
动态内容
:这是将获取的数据,按照字母letter
进行排序即可,这儿不多介绍
friends.sort((a, b) => a.letter.compareTo(b.letter));
联系人列表将头部和尾部合并成一个,数量两个数组调集,在显现内容row
里面别离两个
ListView.builder(
controller: _controller,
itemBuilder: itemForRow,
itemCount: headerItems.length + friends.length,
),
row
完成如下所示
//设置Row,这儿经过巧妙的方法设置section作用
//只有一种带标题的cell,假如letter字母和上一个相同,就不显现标题,不然显现
//这样一种cell就能够表明全部内容
Widget itemForRow(BuildContext context, int index) {
if (index < 4) {
//前四个固定标题的处理
final item = headerItems[index];
return ContactCell(imageUrl: item.imageUrl, text: item.name);
}else {
//后面列表的处理,因为运用的不是一个数组,需求重新调整索引,减去固定标题长度
final item = friends[index - 4];
//动态列表榜首个,一定有标题
if (index == 4) {
return ContactCell(imageUrl: item.imageUrl, text: item.name, isShowLetter: true, letter: item.letter);
}else {
//后面的都和上面一个比较,相同就不显现标题
final lastItem = friends[index - 5];
return ContactCell(imageUrl: item.imageUrl, text: item.name, isShowLetter: item.letter != lastItem.letter, letter: item.letter);
}
}
}
ContactCell
的逻辑如下所示,首要是在基础cell的基础上增加了一个标题头,依据参数操控是否显现,参数由外部操控,letter和上一个相同就不显现标题,不然显现
Column(
children: [
//头部 letter,选用三目运算符操控顶部标题显现,假如letter和上一个相同就不显现
widget.isShowLetter ?
Container(
height: 28,
color: const Color.fromRGBO(0xe1, 0xe1, 0xe1, 1),
padding: const EdgeInsets.only(left: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(widget.letter),
widget.letter == '☆' ? Row(
children: const [
SizedBox(width: 4),
Text('星标朋友',
style: TextStyle(
fontSize: 12
)
)
]
) : Container()
]
)
) : Container(),
//内部item 头像和文字,和发现页类似,逻辑较为简略,省掉
...Container(),
下划线
...line
],
)
索引条
所以条,便是字母letter
调集,将他们纵向布局到右侧即可,然后才是气泡
和跳转指定方位
通信录页面运用 Stack
如下所示,然后包裹了索引条组件 ContactIndexs
Stack(
children: [
ListView.builder(
controller: _controller,
itemBuilder: itemForRow,
itemCount: headerItems.length + friends.length,
),
ContactIndexs(
onUpdateCallback: (String letter, int index) {
print(letter);
jumpToLetter(letter);
},
letterList: letters,
),
],
),
索引的部分完成如下所示,先看看索引条,咱们前面运用了 Stack
布局,便是为了此时,直接运用Positioned
布局将内容放到最右侧
//设置控件相对父布局Stack方位,局右侧,高度拉满
//间隔右、上、下间隔为零,因而左边方位依据内容拉伸
Positioned(
top: 0,
right: 0,
bottom: 0,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//左边气泡,选用alignment的方法调整方位
//直接运用 Stack + Position 索引的方法更简略,运用核算好的点击的letter坐标,减去顶部差即可
//这儿应战一下更杂乱的,选用alignment的方法,按照百分比调整
Container(
//经过alignment来调整方位
alignment: Alignment(-0.15, bubbleAligmentY),
//经过索引更新气泡显隐
child: lastIndex > -1
? Stack(
alignment: const Alignment(-0.15, 0),
children: [
Image.asset(
'images/气泡.png',
width: 60,
height: 60,
),
Text(
widget.letterList[lastIndex],
style: const TextStyle(fontSize: 18),
)
],
)
: null,
),
//右边索引
//经过拖拽手势处理,当按下或许移动的时分,需求显现气泡,且更新外部联系人方位
//一次需求经过拖拽点击的方位,进行核算
GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details) {
//拖拽更新,pan实践走的也是这个,不信看看参数
//世界坐标details.globalPosition
//本地坐标details.localPosition
onUpdate(details.localPosition.dy);
},
onVerticalDragStart: (DragStartDetails details) {
//拖拽点击时
onUpdate(details.localPosition.dy);
},
onVerticalDragEnd: (DragEndDetails details) {
//拖拽完毕
lastIndex = -1;
setState(() {});
},
//处理点击的
onTapDown: (TapDownDetails details) {
onUpdate(details.localPosition.dy);
},
//取消手势,取消后,更新索引,一起能够用索引更新气泡显隐
onTapCancel: () {
lastIndex = -1;
setState(() {});
},
//点击抬起手势
onTapUp: (TapUpDetails details) {
lastIndex = -1;
setState(() {});
},
//放置字母调集
child: SizedBox(
width: 20,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: words,
),
),
),
],
),
);
words
直接生成一个字母组件调集,以便于展开到快速展开到索引中,只需设置好每个字符高度即可,能够更便利核算,配合父组件的Column
默许次轴居中,很容易就完成作用了
//也能够运用 .map来直接回来
List<Widget> words = [];
for (String item in widget.letterList) {
final wordWidget = SizedBox(
height: 14, child: Text(item, style: const TextStyle(fontSize: 10)));
words.add(wordWidget);
}
这儿是索引方位核算的中心逻辑,不仅核算出了点击到的索引是哪一个,还核算出了对应的 alignment百分比
另外气泡运用的是 alignment
,需求先遍及一下, Alignment
特点分别是 x、y
,值域均为-1~1
之间,当一个值为0的时分,默许居中,默许布局中(reverse
的正好相反),-1表明上、左,1表明下、右
int lastIndex = -1; //用于保存上一次点击的letter索引,防止接连回调
double bubbleAligmentY = 0; //用于更新气泡的align
//移动点击更新,能够操控气泡,点击了哪个字母
onUpdate(double dy) {
//核算点击了哪一个,能够回来给外面,每一个高度是14
final length = widget.letterList.length;
// final height = MediaQuery.of(context).size.height; 屏幕高度
final height = context.size!.height; //组件高度
//获取内容高度
const letterHeight = 14;
final allLetterHeight = length * letterHeight;
//获取榜首个索引的开始y坐标
final startY = (height - allLetterHeight) / 2;
//因为核算嗾使是以父节点的空间来响应手势,开始坐标在最上方,之前核算好了榜首个的方位
var index = (dy - startY) ~/ letterHeight; //~/取模,即向下取整
// final index = (length / letterHeight).floor(); //或许这个也行
//因为往上滑或许往下,会出现越界,处理一下越界问题
index = index.clamp(0, length - 1); //能够经过 clamp 方法处理数字越界问题
//假如和上一个不相同,直接回调字母,和更新的索引
if (index != lastIndex) {
widget.onUpdateCallback(widget.letterList[index], index);
}
lastIndex = index;
//核算气泡,内部运用,还没核算
//之前已经核算出了start,用position的方法将会更简略,这儿咱们应战运用alignment,核算百分比
//榜首个字符中间y坐标
final firstLetterCy = startY + letterHeight / 2;
//气泡与榜首个字符中心点对齐需求移动坐标
final halfHeight = height / 2;
//因为中心点是0,上下是-1,所以核算出索引,要用halfHeight才是相当于中心点的坐标
//而物体总移动,还需求抛去自己一半的高度(顶部对齐-1,中心对齐0),所以少移动了半个物体的高度
final radioY =
(halfHeight - firstLetterCy - index * letterHeight) / (halfHeight - 30);
setState(() {
bubbleAligmentY = -radioY;
});
}
到这儿索引条的功用基本完毕了,剩余的便是索引定位到指定的方位,这就和索引条功用无关了
索引条和联系人互动
前面索引条已经定位到点击到的letter
,现在经过点击的letter
定位到指定的字母标题
的方位
如下所示,所以条给了一个回调,回来的点击的字母和索引
//作为署理,用于更新ListView跳转方位
final ScrollController _controller = ScrollController();
Stack(
children: [
ListView.builder(
//署理
controller: _controller,
itemBuilder: itemForRow,
itemCount: headerItems.length + friends.length,
),
ContactIndexs(
onUpdateCallback: (String letter, int index) {
print(letter);
jumpToLetter(letter);
},
letterList: letters,
),
],
)
跳到指定方位,一共三个特点,偏移量、动画时间、过渡动画曲线
jumpToLetter(String letter) {
_controller.animateTo(
lettersLocationMap[letter]!,
duration: const Duration(microseconds: 400),
curve: Curves.easeIn);
}
偏移量的核算如下所示
已知:单个item的高度、顶部固定item的数量、标题的item方位,核算指定标题item的偏移
1、指定标题item偏移
= 顶部固定item高度
+ 上一个组总高度
,然后分解为步骤2
2、指定标题item偏移
: 榜首组则偏移量 = 顶部固定高度 ; 后续偏移量 = 上一组偏移 + 上一组总高度(上一组数量 * index + 标题高度)
//用于保存letter对应的偏移量
Map<String, double> lettersLocationMap = {};
//更新letter的一起,更新偏移量
double offsetY = 47 * 4; //顶部的四个
var offsetIndex = 0; //默许偏移
var isFirst = true;
for (final item in friends) {
if (!letters.contains(item.letter)) {
letters.add(item.letter);
//榜首组的偏移
if (isFirst) {
offsetY += offsetIndex * 47; //更新offsetY
isFirst = false;
}else {
//后续偏移
offsetY += (offsetIndex * 47 + 28);
}
lettersLocationMap[item.letter] = offsetY;
offsetIndex = 0;
}
offsetIndex++;
}
实践数据量小的话,也能够在移动的时分再核算偏移量也不迟
最终
每次的编写与应战,都会让咱们对组件更加了解,运用起来更加得心应手,快来试试吧