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

发布于:2025-05-07 ⋅ 阅读:(12) ⋅ 点赞:(0)

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

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


网站公告

今日签到

点亮在社区的每一天
去签到