flutter 专题 六十一 支持上拉加载更多的自定义横向滑动表格

在股票软件中,经常会看到如下所示的效果(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,
    );

参考:仿同花顺自选股列表

相关推荐
UzumakiHan13 小时前
flutter开发音乐APP(简单的音乐播放demo)
flutter
leluckys17 小时前
flutter 专题 六十四 在原生项目中集成Flutter
flutter
leluckys17 小时前
flutter 专题 一百零四 Flutter环境搭建
flutter
唯鹿17 小时前
AI生成Flutter UI代码实践(一)
人工智能·flutter·ui
仙魁XAN19 小时前
Flutter 学习之旅 之 flutter 有时候部分手机【TextField】无法唤起【输入法软键盘】的一些简单整理
flutter·unity·华为手机·textfield·键盘唤不起
leluckys20 小时前
flutter 专题 五十八 关于Flutter提示Your Xcode project requires migration的错误
flutter
只可远观1 天前
Flutter Dart中的函数参数 默函数的定义 可选参数 箭头函数 匿名函认参数 命名参类数 闭包等
windows·flutter
JarvanMo2 天前
借助FlutterFire CLI实现Flutter与Firebase的多环境配置
前端·flutter
RichardLai882 天前
[Flutter 基础] - Flutter基础组件 - Icon
android·flutter