天长地久有时尽
此恨绵绵无绝期

前言

设计来自项目中搜索模块的更多筛选功能,筛选宜居人数
主要功能:
支持循环滚动、且每次都停留在屏幕中间位置
首尾相连
点击滚动到屏幕中间位置

默认样式

滚动之后样式

设计思路

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,          ),        ),      ),    );  }}