天长地久有时尽
此恨绵绵无绝期
前言
设计来自项目中搜索模块的更多筛选功能,筛选宜居人数
主要功能:支持循环滚动、且每次都停留在屏幕中间位置
首尾相连
点击滚动到屏幕中间位置
默认样式
滚动之后样式
设计思路
ListView.builder滚动视图
NotificationListener监听开始滚动和结束滚动时候的位置
ScrollController控制视图滚动到中间位置
核心逻辑一
NotificationListener监听ListView滚动,
ScrollController滚动到视图中间位置,
isScrollEndNotification解决由于内部_controller.jumpTo方法会无限调用滚动结束事件
// 监听事件
NotificationListener(
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 {
List _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: [
Row(
children: [
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: [
NotificationListener(
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,
),
),
),
);
}
}