「这是我参与11月更文挑战的第4天,活动概况查看:2021终究一次更文挑战」
终究效果
AppBar上的actions
首要,在导航栏上有一个增加朋友的按钮。这个能够运用AppBar的actions
来设置,其次点击这儿的actions
的时分会呼应事情,能够跟之前的发现页面相同,运用GestureDetector
的onTap
手势
class _FriendPageState extends State<FriendPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('通讯录'),
actions: [
GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) =>
DiscoverChildPage(title: '增加朋友')));
},
child: Container(
padding: EdgeInsets.only(right: 10),
child: Image(
image: AssetImage('images/增加朋友.png'),
width: 32,
),
),
)
],
),
body: Center(
child: Text('通讯录页面'),
),
);
}
}
点击右上角的增加按钮
itemBuilder
剖析:这儿自定义的cell需求有4个元素
- 前面四个加载的是本地的图片,这儿需求有一个
assetImage
- 后面是从网络获取,所以这儿需求有一个
imageUrl
- 图片后面的文字
name
- 在Flutter中没有分组的概念,所以需求有
groupTitle
运用Expanded
包装一个Column
上面是名字,下面是分割线
class _FriendCell extends StatelessWidget {
final String? imageUrl;
final String? assetImage;
final String? name;
final String? groupTitle;
_FriendCell({this.imageUrl, this.assetImage, this.name, this.groupTitle});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Row(
children: [
Container(
margin: EdgeInsets.all(10),
width: 34,
height: 34,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
image: DecorationImage(
image: assetImage == null
? NetworkImage(imageUrl!)
: AssetImage(assetImage!) as ImageProvider))),
Container(
child: Text(
name!,
style: TextStyle(fontSize: 18),
),
)
],
),
);
}
}
数据-模型
这儿还没有涉及到网络恳求,所以数据都暂时写在本地,网络恳求的后面再介绍~
class Friends {
final String? imageUrl;
final String? assetImage;
final String? name;
final String? indexLetter;
Friends({this.imageUrl, this.assetImage, this.name, this.indexLetter});
}
List<Friends> datas = [
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053778-be89edf432824c2.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053783-c79efdeac6906cd.jpg',
name: '菲儿',
indexLetter: 'F'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053788-92f98a6d7ef0ba2.jpg',
name: '安莉',
indexLetter: 'A'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053792-d602f6e76a56ac5.jpg',
name: '阿贵',
indexLetter: 'A'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053798-b6216c72f5e4ff2.jpg',
name: '贝拉',
indexLetter: 'B'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1670789167-ca11a3bcf02f9fd.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053803-63f32ada8eaf8d9.jpg',
name: 'Nancy',
indexLetter: 'N'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053808-5f9f3bb94937049.jpg',
name: '扣扣',
indexLetter: 'K'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053812-8e0aecc11a9d41c.jpg',
name: 'Jack',
indexLetter: 'J'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053816-7fde17df202ed3d.jpg',
name: 'Emma',
indexLetter: 'E'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053821-f9c2aa61ee7f584.jpg',
name: 'Abby',
indexLetter: 'A'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053825-16a5081ae45a6b7.jpg',
name: 'Betty',
indexLetter: 'B'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053829-59d57efae4efeb8.jpg',
name: 'Tony',
indexLetter: 'T'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053833-980644221cfc322.jpg',
name: 'Jerry',
indexLetter: 'J'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053838-51304e7df07c6c4.jpg',
name: 'Colin',
indexLetter: 'C'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053842-68307704edacc67.jpg',
name: 'Haha',
indexLetter: 'H'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053846-b5c9a0d86593a19.jpg',
name: 'Ketty',
indexLetter: 'K'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053850-77cc895c5d2bed5.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://www.6hu.cc/wp-content/uploads/2022/12/1671053855-9bf6a6730e5b1be.jpg',
name: 'Lina',
indexLetter: 'L'),
];
ListView
好了,有了数据模型和itemBulider,咱们创建ListView就很简单了。当index<4的时分加载的是本地的assetImage
反之加载的是网络图片imageUrl
import 'package:flutter/material.dart';
import 'package:flutter/src/painting/image_provider.dart';
import 'discover_child_page.dart';
import 'friend_data.dart';
class FriendPage extends StatefulWidget {
const FriendPage({Key? key}) : super(key: key);
@override
_FriendPageState createState() => _FriendPageState();
}
class _FriendPageState extends State<FriendPage> {
final List<Friends> _headerData = [
Friends(assetImage: 'images/新的朋友.png', name: '新的朋友'),
Friends(assetImage: 'images/群聊.png', name: '群聊'),
Friends(assetImage: 'images/标签.png', name: '标签'),
Friends(assetImage: 'images/大众号.png', name: '大众号'),
];
Widget _itemForRow(BuildContext context, int index) {
if (index < _headerData.length) {
return _FriendCell(
assetImage: _headerData[index].assetImage,
name: _headerData[index].name);
} else {
return _FriendCell(
imageUrl: datas[index - 4].imageUrl,
name: datas[index - 4].name,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color.fromRGBO(238, 238, 238, 1),
appBar: AppBar(
title: Text('通讯录'),
actions: [
GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) =>
DiscoverChildPage(title: '增加朋友')));
},
child: Container(
padding: EdgeInsets.only(right: 10),
child: Image(
image: AssetImage('images/增加朋友.png'),
width: 32,
),
),
)
],
),
body: Container(
child: ListView.builder(
itemBuilder: _itemForRow,
itemCount: datas.length + _headerData.length),
));
}
}
分组
-groupTitle
由由于LIstView没有分组的概念,所以这儿增加一个头部视图,依据条件来主动的显示和隐藏来直接到达分组的意图。咱们能够运用for循环多添点数据生成的新的数组排序之后再赋值给Cell
// 下面数据源
final List<Friends> _listData = [];
final List<Friends> _headerData = [
Friends(assetImage: 'images/新的朋友.png', name: '新的朋友'),
Friends(assetImage: 'images/群聊.png', name: '群聊'),
Friends(assetImage: 'images/标签.png', name: '标签'),
Friends(assetImage: 'images/大众号.png', name: '大众号'),
];
@override
void initState() {
// TODO: implement initState
super.initState();
_listData..addAll(datas)..addAll(datas);
_listData.sort((Friends a, Friends b) {
return a.indexLetter!.compareTo(b.indexLetter!);
});
}
这样,就有了一个排好序之后的数据源。咱们在cell中的处理_itemForRow
:如果首字母相同,那么就没有groupTitle
;如果首字母不相同也就是要分组啦,就展现这个groupTitle
Widget _itemForRow(BuildContext context, int index) {
if (index < _headerData.length) {
return _FriendCell(
assetImage: _headerData[index].assetImage,
name: _headerData[index].name);
} else {
bool _hiddenGroupTitle = index - 4 > 0 &&
_listData[index - 4].indexLetter == _listData[index - 5].indexLetter;
return _FriendCell(
imageUrl: _listData[index - 4].imageUrl,
name: _listData[index - 4].name,
groupTitle: _hiddenGroupTitle ? null : _listData[index - 4].indexLetter,
);
}
}
那么此刻就要修正_FriendCell
的布局啦,不能运用Row需求运用Column了,咱们修正下吧,在Column的Children中新增一个头部视图,这个头部视图的高度由groupTitle
的值来控制。
Container(
padding: EdgeInsets.only(left: 10),
alignment: Alignment.centerLeft,
height: groupTitle == null ? 0 : 20,
color: Color.fromRGBO(238, 238, 238, 1),
child: groupTitle == null
? null
: Text(
groupTitle!,
style: TextStyle(fontSize: 17, color: Colors.grey),
),
),
索引条
右边的索引条是固定在屏幕的右边,所以此刻要运用Stack布局,第一个是ListView,第二个是索引条。这儿运用Positioned
布局,设置好上右边距和高度以及宽度即可
Positioned(
child: Column(
children: _widgetData,
),
right: 0,
top: screenHeight(context) / 8,
height: screenHeight(context) / 2,
width: 30,
)
这儿的_widgetData是一个Widget的数组,数据也是来自于本地
const INDEX_WORDS = [
'',
'☆',
'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'
];
然后在initState()
办法里边for循环创建Text组件,文字运用INDEX_WORDS[i]
,注意这儿的在Column的结构上占满,所以此刻创建Text的时分运用Expanded
包装一层
索引条增加事情
望文生义,这儿肯定是要用到GestureDetector
这个类,在DragDown
的时分索引条的布景变黑,文字变成白色,在DragEnd
的时分康复如初。考虑到这个导航条会比较复杂建议直接抽取一个index_bar
文件
首要声明两个记载当时色彩的值
Color _backColor = Color.fromRGBO(1, 1, 1, 0);
Color _textColor = Colors.grey;
接着在手势拖拽状态发生改变的时分,修正这两个值的色彩,一起把当时的值赋值给Widget
GestureDetector(
onVerticalDragDown: (DragDownDetails details) {
setState(() {
_backColor = Color.fromRGBO(1, 1, 1, 0.5);
_textColor = Colors.white;
});
},
onVerticalDragEnd: (DragEndDetails details) {
setState(() {
_backColor = Color.fromRGBO(1, 1, 1, 0);
_textColor = Colors.grey;
});
},
onVerticalDragUpdate: (DragUpdateDetails details) {
String str = getIndexWord(context, details);
print('选中的是' + str);
},
child: Container(
child: Column(children: _widgetData),
color: _backColor, // 布景色彩赋值
),
),
要想把_textColor
实时的赋值给当时的Text,那么此刻Text的初始化就要放到Build办法里边,而不是initState
这儿了。这儿重点介绍下找到当时点击的Index,能够经过计算偏移量/每个字符的高度
来拿到。 完整代码如下:
import 'package:flutter/material.dart';
import 'const_data.dart';
import 'friend_data.dart';
class IndexBar extends StatefulWidget {
@override
_IndexBarState createState() => _IndexBarState();
}
class _IndexBarState extends State<IndexBar> {
Color _backColor = Color.fromRGBO(1, 1, 1, 0);
Color _textColor = Colors.grey;
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
final List<Widget> _widgetData = [];
for (int i = 0; i < INDEX_WORDS.length; i++) {
_widgetData.add(Expanded(
child: Text(INDEX_WORDS[i],
style: TextStyle(fontSize: 10, color: _textColor))));
}
return Positioned(
right: 0,
top: screenHeight(context) / 8,
height: screenHeight(context) / 2,
width: 30,
child: GestureDetector(
onVerticalDragDown: (DragDownDetails details) {
setState(() {
_backColor = Color.fromRGBO(1, 1, 1, 0.5);
_textColor = Colors.white;
});
},
onVerticalDragEnd: (DragEndDetails details) {
setState(() {
_backColor = Color.fromRGBO(1, 1, 1, 0);
_textColor = Colors.grey;
});
},
onVerticalDragUpdate: (DragUpdateDetails details) {
String str = getIndexWord(context, details);
print('选中的是' + str);
},
child: Container(
child: Column(children: _widgetData),
color: _backColor,
),
),
);
}
}
String getIndexWord(BuildContext context, DragUpdateDetails details) {
// 找到当时渲染目标
RenderBox box = context.findRenderObject() as RenderBox;
// offset,globalToLocal当时方位间隔父视图的偏移
Offset y = box.globalToLocal(details.globalPosition);
// 算出字符高度
var itemHeight = screenHeight(context) / 2 / INDEX_WORDS.length;
// 算出第几个item ~/代表取整 clamp函数给定最大和最小值
int index = (y.dy ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
return INDEX_WORDS[index];
}
终究贴上资料和代码的地址: