在股票软件中,经常会看到如下所示的效果(ps:由于公司数据敏感,所以使用另一个朋友的一个图)。
分析需要后,我先在网上找了下支持横向滑动的组件,最后找到了这个:flutter_horizontal_data_table,看了下示例,也满足我的开发需要,并且我使用2000条数据进行测试,也没有卡顿的问题。
不过,这个组件有一个问题是不支持下拉,因为很多场景中,对于这种数据比较多的情况,我们需要对数据进行分页加载,给予此,我们需要对flutter_horizontal_data_table进行改造,增加支持上拉加载更多和下拉刷新的功能。于是,改造后的代码如下所示。
/*
* https://github.com/MayLau-CbL/flutter_horizontal_data_table
*/
class HorizontalDataTable extends StatefulWidget {
final VoidCallback loadMore;
final bool enablePullUp;
final double leftHandSideColumnWidth;
final double rightHandSideColumnWidth;
final bool isFixedHeader;
final List<Widget> headerWidgets;
final List<Widget> leftSideChildren;
final List<Widget> rightSideChildren;
final int itemCount;
final IndexedWidgetBuilder leftSideItemBuilder;
final IndexedWidgetBuilder rightSideItemBuilder;
final Widget rowSeparatorWidget;
final double elevation;
const HorizontalDataTable(
{@required this.leftHandSideColumnWidth,
@required this.rightHandSideColumnWidth,
this.isFixedHeader = false,
this.headerWidgets,
this.leftSideItemBuilder,
this.rightSideItemBuilder,
this.itemCount = 0,
this.leftSideChildren,
this.rightSideChildren,
this.enablePullUp = false,
this.loadMore,
this.rowSeparatorWidget = const Divider(
color: Colors.transparent,
height: 0.0,
thickness: 0.0,
),
this.elevation = 5.0,
Key key})
: assert(
(leftSideChildren == null && leftSideItemBuilder != null) ||
(leftSideChildren == null),
'Either using itemBuilder or children to assign left side widgets'),
assert(
(rightSideChildren == null && rightSideItemBuilder != null) ||
(rightSideChildren == null),
'Either using itemBuilder or children to assign right side widgets'),
assert((isFixedHeader && headerWidgets != null) || !isFixedHeader,
'If use fixed top row header, isFixedHeader==true, headerWidgets must not be null'),
assert(itemCount >= 0, 'itemCount must >= 0'),
assert(elevation >= 0.0, 'elevation must >= 0.0'),
super(key: key);
@override
State<StatefulWidget> createState() {
return HorizontalDataTableState();
}
}
class HorizontalDataTableState extends State<HorizontalDataTable> {
ScrollController _leftHandSideListViewScrollController = ScrollController(keepScrollOffset: false);
ScrollController _rightHandSideListViewScrollController = ScrollController(keepScrollOffset: false);
ScrollController _rightHorizontalScrollController = ScrollController(keepScrollOffset: false);
_SyncScrollControllerManager _syncScroller = _SyncScrollControllerManager();
ScrollShadowModel _scrollShadowModel = ScrollShadowModel();
RefreshController refreshController = RefreshController();
ScrollController refreshScrollController = ScrollController(keepScrollOffset: false);
bool finishLoading=true;
scrollToTop() {
_leftHandSideListViewScrollController.jumpTo(0);
}
finishLoad() {
finishLoading = true;
}
@override
void initState() {
super.initState();
_syncScroller
.registerScrollController(_leftHandSideListViewScrollController);
_syncScroller
.registerScrollController(_rightHandSideListViewScrollController);
_leftHandSideListViewScrollController.addListener(() {
_scrollShadowModel.verticalOffset =
_leftHandSideListViewScrollController.offset;
if(_leftHandSideListViewScrollController.position.pixels + 90 >=_leftHandSideListViewScrollController.position.maxScrollExtent) {
if(finishLoading) {
this.widget.loadMore();
finishLoading = false;
print('HorizontalDataTableState>>>>>>>');
}
}else {
setState(() {});
}
});
_rightHorizontalScrollController.addListener(() {
_scrollShadowModel.horizontalOffset =
_rightHorizontalScrollController.offset;
if(_rightHorizontalScrollController.position.pixels == _rightHorizontalScrollController.position.maxScrollExtent) {
}else {
setState(() {});
}
});
}
@override
void dispose() {
_syncScroller
.unregisterScrollController(_leftHandSideListViewScrollController);
_syncScroller
.unregisterScrollController(_rightHandSideListViewScrollController);
_leftHandSideListViewScrollController.dispose();
_rightHandSideListViewScrollController.dispose();
_rightHorizontalScrollController.dispose();
refreshScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _buildContent();
}
Widget buildClassicFooter() {
return CustomFooter(
height: 0,
builder: (BuildContext context,LoadStatus mode){
return Container();
},
);
}
Widget _buildContent() {
return ChangeNotifierProvider<ScrollShadowModel>.value(value: _scrollShadowModel, child: SafeArea(
child: _getParallelListView()
));
}
Widget _getParallelListView() {
return Row(
children: <Widget>[
Consumer<ScrollShadowModel>(
child: Container(
width: widget.leftHandSideColumnWidth,
child: _getLeftSideFixedHeaderScrollColumn(),
),
builder: (context, scrollShadowModel, child) {
return Material(
child: child,
elevation: _getElevation(scrollShadowModel.horizontalOffset),
);
},
),
Expanded(
child: SingleChildScrollView(
controller: _rightHorizontalScrollController,
child: Container(
child: _getRightSideHeaderScrollColumn(),
width: widget.rightHandSideColumnWidth,
),
scrollDirection: Axis.horizontal,
),
)
],
);
}
Widget _getLeftSideFixedHeaderScrollColumn() {
if (widget.isFixedHeader) {
return Column(
children: <Widget>[
Consumer<ScrollShadowModel>(
child: widget.headerWidgets[0],
builder: (context, scrollShadowModel, child) {
return Material(
child: child,
elevation: _getElevation(scrollShadowModel.verticalOffset),
);
},
),
widget.rowSeparatorWidget,
Expanded(
child: _getScrollColumn(_getLeftHandSideListView(),
this._leftHandSideListViewScrollController)),
],
);
} else {
return _getScrollColumn(_getLeftHandSideListView(),
this._leftHandSideListViewScrollController);
}
}
Widget _getRightSideHeaderScrollColumn() {
if (widget.isFixedHeader) {
List<Widget> widgetList = List<Widget>();
//headers
widgetList.add(Consumer<ScrollShadowModel>(
builder: (context, scrollShadowModel, child) {
return Material(
child: child,
elevation: _getElevation(scrollShadowModel.verticalOffset));
},
child: Row(children: widget.headerWidgets.sublist(1))));
widgetList.add(
widget.rowSeparatorWidget,
);
//ListView
widgetList.add(Expanded(
child: _getScrollColumn(_getRightHandSideListView(),
this._rightHandSideListViewScrollController),
));
return Column(
children: widgetList,
);
} else {
return _getScrollColumn(_getRightHandSideListView(),
this._rightHandSideListViewScrollController);
}
}
Widget _getScrollColumn(Widget child, ScrollController scrollController) {
return NotificationListener<ScrollNotification>(
child: child,
onNotification: (ScrollNotification scrollInfo) {
_syncScroller.processNotification(scrollInfo, scrollController);
return false;
},
);
}
Widget _getRightHandSideListView() {
return _getListView(
_rightHandSideListViewScrollController,
widget.rightSideItemBuilder,
widget.itemCount,
widget.rightSideChildren);
}
Widget _getLeftHandSideListView() {
return _getListView(_leftHandSideListViewScrollController,
widget.leftSideItemBuilder, widget.itemCount, widget.leftSideChildren);
}
Widget _getListView(ScrollController scrollController,
IndexedWidgetBuilder indexedWidgetBuilder, int itemCount,
[List<Widget> children]) {
if (indexedWidgetBuilder != null) {
return ListView.separated(
controller: scrollController,
itemBuilder: indexedWidgetBuilder,
itemCount: itemCount,
separatorBuilder: (context, index) {
return widget.rowSeparatorWidget;
},
);
} else {
return ListView(
controller: scrollController,
children: children,
);
}
}
double _getElevation(double offset) {
return 0.0;
}
}
class _SyncScrollControllerManager {
List<ScrollController> _registeredScrollControllers =
new List<ScrollController>();
ScrollController _scrollingController;
bool _scrollingActive = false;
void registerScrollController(ScrollController controller) {
_registeredScrollControllers.add(controller);
}
void unregisterScrollController(ScrollController controller) {
_registeredScrollControllers.remove(controller);
}
void processNotification(
ScrollNotification notification, ScrollController sender) {
if (notification is ScrollStartNotification && !_scrollingActive) {
_scrollingController = sender;
_scrollingActive = true;
return;
}
if (identical(sender, _scrollingController) && _scrollingActive) {
if (notification is ScrollEndNotification) {
_scrollingController = null;
_scrollingActive = false;
return;
}
if (notification is ScrollUpdateNotification) {
_registeredScrollControllers.forEach((controller) {
if (!identical(_scrollingController, controller)) {
if (controller.hasClients) {
controller.jumpTo(_scrollingController.offset);
} else {}
}
});
return;
}
}
}
}
在Flutter中,为了支持下拉和上拉功能,我们可以使用SmartRefresher
组件或者RefreshIndicator
来实现,不过,我试了下,效果并不好,至于为什么,大家可以自己试一下。最后,为了满足需求,我使用可一个比较投机的方式,即使用ScrollController
。我们知道,任何滚动监听都可以使用ScrollController
来实现。如果要获取ScrollController距离坐标原点的位置可以使用如下的方式进行获取。
scrollController.position.pixels
由于ScrollController一般都会配合一个列表组件来使用,所以,我们可以使用下面的方法来获取列表底部距离坐标原点的值。
scrollController.position.maxScrollExtent
基于这个原理,我们可以在列表滚动到列表底部之前,请求下一页的数据,即我们可以进行如下的判断。
if(_leftHandSideListViewScrollController.position.pixels -_leftHandSideListViewScrollController.position.maxScrollExtent>= 90 ) {
if(finishLoading) {
this.widget.loadMore();
finishLoading = false;
}
}
上面的数值90是可以修改的。并且,如果当前是正在上拉状态,是不可以再请求的,因此我们需要设置一个状态标志。然后,在需要使用时,传入对应的参数即可。
double length = Strings.totalItemName.length * width;
return Container(
color: Colors.white,
child: HorizontalDataTable(
key: this.widget.globalKey,
enablePullUp: true,
loadMore: () {
this.widget.loadMore(); //关键代码,上拉加载更多
},
leftHandSideColumnWidth: 130,
rightHandSideColumnWidth: length,
isFixedHeader: true,
headerWidgets: _getTitleWidget(),
leftSideItemBuilder: _generateFirstColumnRow,
rightSideItemBuilder: _generateRightHandSideColumnRow,
itemCount: widget.datas.length,
rowSeparatorWidget: Divider(
color: Colors.black54,
height: 1.0,
thickness: 0.0,
),
),
height: MediaQuery.of(context).size.height,
);
参考:仿同花顺自选股列表