Flutter仿微信通讯录实现自定义导航条的示例代码
某些页面比如我们在选择联系人或者某个城市的时候需要快速定位到我们需要的选项,一般都会需要像微信通讯录右边有一个导航条一样的功能,由A到Z进行快速定位,本篇文章我们将自己来实现一个跟微信通讯录同样的功能。
关键点:手势定位滑动、列表定位、手势、列表联动。
准备数据,首先我们需要准备导航目录数据,
List<String> _az = [ "☆", "A", "B", "C", "D", "E", "F", "G", "H", "I", "G", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", ];
然后列表数据,列表数据可以让后台给我们排序好,如果后台不给我们排序,我们也可以自己排序进行组装, 这里用到一个插件,根据汉字获取拼音首字母,我们自己就可以对这些数据进行整理排序。
lpinyin: ^2.0.3
数据格式:一个NameBean对应一个字母所在的列表,这是我们存储正式列表数据的格式。
class NameBean { String? initial;// 字母导航 List<Name>? nameList;// 内容列表 NameBean({ this.initial, this.nameList, }); } /// name : "老李" /// 这里我只放了一个name字段,以后扩展内容只需在这里新增字段就好了 class Name { String? name; Name({ this.name, }); }
接下来我们先做导航条:
实现思路: 导航条就是一个List列表,点击、滑动、松开会有不同的交互,我们根据不同的交互来进行实现,这里我们会用到官方的GestureDetector组件,专门用来进行手势识别。将每一个item的高度固定,通过手势交互返回的位置数据来进行返回我们想要的目录。比如我的每一个目录高度设置为20.
导航条代码:点击或者移动的时候选中的目录会有一个按压效果,
GestureDetector( child: Container( margin: EdgeInsetsDirectional.only(top: 40), width: 40, // 导航条 child: ListView.builder( physics: NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { return SizedBox( height: 20, // 这里做了一个按压或移动滑动的触发效果 child: Container( alignment: Alignment.center, decoration: BoxDecoration( color: currentIndex == index ? Colors.redAccent : null, shape: BoxShape.circle), child: Text( _az[index], style: TextStyle( color: currentIndex == index ? Colors.white : Colors.black87, ), ), ), ); }, itemCount: _az.length, )), //手指按下触发 竖着划就用onVertica XXX回调 onVerticalDragDown: (DragDownDetails e) { //打印手指按下的位置(相对于屏幕) int i = e.localPosition.dy ~/ 20; // _scrollController.jumpTo(index: i); setState(() { currentIndex = i; }); }, //手指滑动时会触发此回调 onVerticalDragUpdate: (DragUpdateDetails e) { //用户手指滑动时,更新偏移 int i = e.localPosition.dy ~/ 20; _az.length; if (i >= 0 && i <= _az.length - 1) { if (i != currentIndex) { setState(() { // 当前选中的index 默认-1 currentIndex = i; }); print("滑动 ${_az[i]}"); } } }, // 手指抬起 onTapUp: (e) { // 手指抬起 setState(() { currentIndex = -1; }); }, // 移动取消 onVerticalDragEnd: (e) { // 移动取消 setState(() { currentIndex = -1; }); }, )
然后我们可以看到微信在滑动的时候有个字母放大气泡会跟随着手势移动。
实现思路:
气泡和导航条并列,并根据手势位置更新上边距即可,因为我们的导航条的每一个item的高度是固定的,所以我们就可以根据滑动的位置计算出滑动距离顶部的高度,这里气泡可以让UI切个背景图,也可以自己用canvas画一个。
气泡绘制源码:目前我在学习绘制组件,顺便画了一个,可能不是最佳的,但这不重要~今天的重点不是它~~~
@override void paint(Canvas canvas, Size size) { // 原点移到左下角 canvas.translate(size.width / 2, size.height / 2); Paint paint = Paint() ..color = Colors.redAccent ..strokeWidth = 2 ..style = PaintingStyle.fill; Path path = Path(); // 绘制文字 path.lineTo(0, -size.width / 2); // path.conicTo(33, -28, 20, 0, 1); path.arcToPoint(Offset(size.width / 2, 0), radius: Radius.circular(size.width / 2), largeArc: true, clockwise: true); path.close(); var bounds = path.getBounds(); canvas.save(); canvas.translate(-bounds.width / 2, bounds.height / 2); canvas.rotate(pi * 1.2); canvas.drawPath(path, paint); canvas.restore(); // 绘制文字 var textPainter = TextPainter( text: TextSpan( text: text, style: TextStyle( fontSize: 24, foreground: Paint() ..style = PaintingStyle.fill ..color = Colors.white, )), textAlign: TextAlign.center, textDirection: TextDirection.ltr); textPainter.layout(); canvas.translate(-size.width, -size.height / 2); textPainter.paint(canvas, Offset(-size.width / 2.4, size.height / 1.2)); }
导航条、气泡都完成了,接下来就非常简单了,内容的填充我们就不能用ListView了,这里我们需要一个官方的插件: scrollable_positioned_list: ^0.2.3
使用它可以定位到具体的item位置,这样我们就可以进行列表定位和导航条进行联动了。
内容代码:
ScrollablePositionedList.builder( physics: BouncingScrollPhysics(), itemScrollController: _scrollController, itemBuilder: (context, index) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( padding: EdgeInsetsDirectional.only(start: 10), child: Text(dataList[index].initial ?? ""), color: Colors.grey.shade300, height: 30, width: double.infinity, alignment: Alignment.centerLeft, ), Container( padding: EdgeInsetsDirectional.only(start: 15), child: ListView.builder( // 禁用滑动事件 physics: NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, cityIndex) { return InkWell( child: Container( height: 40, child: Column( children: [ Expanded( child: Container( child: Text(dataList[index] .nameList?[cityIndex] .name ?? ""), alignment: Alignment.centerLeft, ), ), Divider( height: 0.5, ) ], ), ), onTap: () {}, ); }, itemCount: dataList[index].nameList?.length, ), ) ], ); }, itemCount: dataList.length, )
ScrollablePositionedList用法基本和ListView一直,只是它多了一个这个方法,可以定位到具体的item。
_scrollController.jumpTo(index: i);
看下最终效果:请忽略数据的重复...手动填充数据太麻烦了,有哪里不懂可以交流哦
总结:
通过这个组件我们可以简单的了解Flutter的手势交互操作,通过手势识别我们可以实现很多有意思的组件,尤其结合绘制和动画可以做出来非常有意思的交互,所有的交互最终的底层都是通过手势识别完成的,以后有时间研究下源码再和大家分享~