阅读 117

Flutter实战-搭建微信项目(四)

最终效果

image.png

AppBar上的actions

首先,在导航栏上有一个添加朋友的按钮。这个可以使用AppBar的actions来设置,其次点击这里的actions的时候会响应事件,可以跟之前的发现页面一样,使用GestureDetectoronTap手势

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('通讯录页面'),       ),     );   } } 复制代码

image.png

点击右上角的添加按钮

image.png

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://randomuser.me/api/portraits/women/27.jpg',       name: 'Lina',       indexLetter: 'L'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/17.jpg',       name: '菲儿',       indexLetter: 'F'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/16.jpg',       name: '安莉',       indexLetter: 'A'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/men/31.jpg',       name: '阿贵',       indexLetter: 'A'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/22.jpg',       name: '贝拉',       indexLetter: 'B'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/37.jpg',       name: 'Lina',       indexLetter: 'L'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/18.jpg',       name: 'Nancy',       indexLetter: 'N'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/men/47.jpg',       name: '扣扣',       indexLetter: 'K'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/men/3.jpg',       name: 'Jack',       indexLetter: 'J'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/5.jpg',       name: 'Emma',       indexLetter: 'E'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/24.jpg',       name: 'Abby',       indexLetter: 'A'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/men/15.jpg',       name: 'Betty',       indexLetter: 'B'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/men/13.jpg',       name: 'Tony',       indexLetter: 'T'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/men/26.jpg',       name: 'Jerry',       indexLetter: 'J'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/men/36.jpg',       name: 'Colin',       indexLetter: 'C'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/12.jpg',       name: 'Haha',       indexLetter: 'H'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/11.jpg',       name: 'Ketty',       indexLetter: 'K'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/13.jpg',       name: 'Lina',       indexLetter: 'L'),   Friends(       imageUrl: 'https://randomuser.me/api/portraits/women/23.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),         ));   } } 复制代码

image.png

分组-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),              ),  ), 复制代码

image.png

索引条

右边的索引条是固定在屏幕的右边,所以此时要使用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包装一层

image.png

索引条添加事件

顾名思义,这里肯定是要用到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]; } 复制代码

image.png


作者:weak_PG
链接:https://juejin.cn/post/7028780514769125383

文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐