天长地久有时尽
此恨绵绵无绝期
前言
设计来自项目中搜索模块的更多筛选功能,筛选宜居人数
主要功能:支持循环滚动、且每次都停留在屏幕中间位置
首尾相连
点击滚动到屏幕中间位置
默认样式
滚动之后样式
设计思路
ListView.builder滚动视图
NotificationListener监听开始滚动和结束滚动时候的位置
ScrollController控制视图滚动到中间位置
核心逻辑一
NotificationListener监听ListView滚动,
ScrollController滚动到视图中间位置,
isScrollEndNotification解决由于内部_controller.jumpTo方法会无限调用滚动结束事件
// 监听事件 NotificationListener<ScrollNotification>( child: ListView.builder( controller: _controller, itemCount: _list.length * 10000,// 初始化10000个item itemExtent: width / 7, itemBuilder: (BuildContext context, int index) { // index % _list.length 无限轮播 return _listViewItem(_list[index % _list.length], index, singleItemWidth); }, scrollDirection: Axis.horizontal, ), onNotification: (ScrollNotification notification) { // 开始滚动的监听事件 if(notification is ScrollStartNotification) { isScrollEndNotification = false; _startLocation = notification.metrics.pixels; } // 滚动结束的监听事件 if (notification is ScrollEndNotification && !isScrollEndNotification) { _endLocation = notification.metrics.pixels; isScrollEndNotification = true; double differ = _endLocation-_startLocation; double offset = 0; if(differ>0) { offset = (differ.abs()~/singleItemWidth)*singleItemWidth; if(differ%singleItemWidth >= singleItemWidth/2) { offset += singleItemWidth; } // _controller滚到中间的位置, _controller.jumpTo(_startLocation + offset); } else if(differ<0){ differ = differ.abs(); offset = ((differ~/singleItemWidth)*singleItemWidth); if((differ%singleItemWidth) >= (singleItemWidth/2)) { offset += singleItemWidth; } // _controller滚到中间的位置 _controller.jumpTo(_startLocation - offset); } } double result = notification.metrics.pixels/singleItemWidth; int round = result.round();// 四舍五入 // 计算索引并返回给外部 widget.slideAction(round%12);// 取余之后返回索引 return true; }, ),
核心逻辑二
每个item对应的widget
点击item滚动到视图中间位置
Widget _listViewItem(String title, int index, double singleItemWidth) { return GestureDetector( onTap: (){ // 滚动到中间位置 double offset = (index-3)*singleItemWidth; _controller.jumpTo(offset); widget.slideAction((index-3)%12); }, child: Container( color: Colors.white, alignment: Alignment.center, child: Text( title, style: TextStyle( color: ColorUtil.color('#212121'), fontSize: 12, ), ), ), ); }
全部源码
import 'package:flutter/material.dart';// 一个颜色的三方插件import 'package:flutter_color_plugin/flutter_color_plugin.dart';class HousePerson extends StatefulWidget { final int selectIndex;// 外部传入默认选择第几个 final Function slideAction; // 滚动停止的回调方法,给外部传选中的索引值 bool clearData; // 支持清楚数据功能,true代表恢复默认样式 HousePerson({this.slideAction, this.selectIndex, this.clearData}); @override _HousePersonState createState() => _HousePersonState();}class _HousePersonState extends State<HousePerson> { List<String> _list = [ '9人', '10人', '10+', '不限', '1人', '2人', '3人', '4人', '5人', '6人', '7人', '8人', ]; bool isScrollEndNotification = false; ScrollController _controller; double _startLocation = 0; double _endLocation = 0; @override void initState() { // TODO: implement initState super.initState(); } @override void didChangeDependencies() { super.didChangeDependencies(); double screenWidth = MediaQuery.of(context).size.width; int select = widget.selectIndex > 0 ? widget.selectIndex : 0; _controller = ScrollController( initialScrollOffset: (3000+select) * (screenWidth - 40) / 7, ); } @override void dispose() { //为了避免内存泄露,需要调用_controller.dispose _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; if(widget.clearData) { _controller.jumpTo(3000 * (screenWidth - 40) / 7); widget.clearData = false; } return Container( color: Colors.white, child: Column( children: <Widget>[ Row( children: <Widget>[ SizedBox( width: 20, ), Text( '最多宜居', style: TextStyle( fontSize: 15, color: ColorUtil.color('#212121'), fontFamily: 'PingFangSC-Semibold', fontWeight: FontWeight.bold, ), ), ], ), SizedBox( height: 15, ), _line(screenWidth - 40), Container( width: screenWidth - 40, color: Colors.white, height: 50, child: _listView(screenWidth - 40), ), _line(screenWidth - 40), ], ), ); } Widget _line(double width) { return Container( width: width, height: 0.5, color: ColorUtil.color('#BDBDBD'), ); } Widget _listView(double width) { double singleItemWidth = width/7; return Stack( children: <Widget>[ NotificationListener<ScrollNotification>( child: ListView.builder( controller: _controller, itemCount: _list.length * 10000, itemExtent: width / 7, itemBuilder: (BuildContext context, int index) { return _listViewItem(_list[index % _list.length], index, singleItemWidth); }, scrollDirection: Axis.horizontal, ), onNotification: (ScrollNotification notification) { if(notification is ScrollStartNotification) { isScrollEndNotification = false; _startLocation = notification.metrics.pixels; } if (notification is ScrollEndNotification && !isScrollEndNotification) { _endLocation = notification.metrics.pixels; isScrollEndNotification = true; double differ = _endLocation-_startLocation; double offset = 0; if(differ>0) { offset = (differ.abs()~/singleItemWidth)*singleItemWidth; if(differ%singleItemWidth >= singleItemWidth/2) { offset += singleItemWidth; } _controller.jumpTo(_startLocation + offset); } else if(differ<0){ differ = differ.abs(); offset = ((differ~/singleItemWidth)*singleItemWidth); if((differ%singleItemWidth) >= (singleItemWidth/2)) { offset += singleItemWidth; } _controller.jumpTo(_startLocation - offset); } } double result = notification.metrics.pixels/singleItemWidth; int round = result.round();// 四舍五入 widget.slideAction(round%12);// 取余之后返回索引 return true; }, ), Positioned( left: width / 2 - 15, top: 0, child: Container( width: 30, height: 3, color: ColorUtil.color('#FED836'), ), ), Positioned( left: width / 2 - 15, bottom: 0, child: Container( width: 30, height: 3, color: ColorUtil.color('#FED836'), ), ), ], ); } Widget _listViewItem(String title, int index, double singleItemWidth) { return GestureDetector( onTap: (){ // 滚动到中间位置 double offset = (index-3)*singleItemWidth; _controller.jumpTo(offset); widget.slideAction((index-3)%12); }, child: Container( color: Colors.white, alignment: Alignment.center, child: Text( title, style: TextStyle( color: ColorUtil.color('#212121'), fontSize: 12, ), ), ), ); }}