在股票軟件中,經常會看到如下所示的效果(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);@overrideState<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;}@overridevoid 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(() {});}});}@overridevoid dispose() {_syncScroller.unregisterScrollController(_leftHandSideListViewScrollController);_syncScroller.unregisterScrollController(_rightHandSideListViewScrollController);_leftHandSideListViewScrollController.dispose();_rightHandSideListViewScrollController.dispose();_rightHorizontalScrollController.dispose();refreshScrollController.dispose();super.dispose();}@overrideWidget 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>();//headerswidgetList.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,);//ListViewwidgetList.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,);
參考:仿同花順自選股列表