Flutter HorizontalDataTable源码解读

HorizontalDataTable地址在这:
https://github.com/MayLau-CbL/flutter_horizontal_data_table

看源码的初衷

这货是个可以横向滚动的表格,然后写的也挺好的,但是这种表格数据都很大,不可能一次性加载完,所以就需要上拉加载功能。然后我就把pull_to_refresh嵌套着试试,发现不行,只有拉表头和表尾才能上拉加载,无奈就只能看源码自己实现了,先上图看我自己实现的效果,虽然很丑,但至少功能有了。

Flutter HorizontalDataTable源码解读_第1张图片

至于具体实现就不说了,比较丑陋,能用即可,哦,对了,我是来源码解读了,开始吧。

正题

先看下构造方法吧

const HorizontalDataTable({
    @required this.leftHandSideColumnWidth,///左侧表格宽度
    @required this.rightHandSideColumnWidth,///右侧表格宽度
    this.isFixedHeader = false,///是否显示表格头部
    this.headerWidgets,///表格头部要显示的widgets
    this.leftSideItemBuilder,///左侧表格要显示的widgetbuilder
    this.rightSideItemBuilder,///右侧表格要显示的widgetbuilder
    this.itemCount = 0,///表格行数
    this.leftSideChildren,///这个和leftSideItemBuilder功能一样,只会用一个
    this.rightSideChildren,///这个和rightSideItemBuilder功能一样,只会用一个
    this.rowSeparatorWidget = const Divider(
      color: Colors.transparent,
      height: 0.0,
      thickness: 0.0,
    ),///row之间的分割线
    this.elevation = 3.0,///滑动后阴影的效果
    this.elevationColor = Colors.black54,///滑动后阴影的颜色
    this.leftHandSideColBackgroundColor = Colors.white,///左侧表格背景色
    this.rightHandSideColBackgroundColor = Colors.white,///右侧表格背景色
  })
      : 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'),
        assert(elevationColor != null, 'elevationColor must not be null');

每个属性的作用在备注里边已经注释,一看就知道怎么用了,那么我们开始看重点。

框架

先了解一下整个框架,如图


Flutter HorizontalDataTable源码解读_第2张图片
图片.png

图中红框事整个widget,其实是个Stack,里边放了两个Positioned,即绿框和蓝框,具体的可以用如下代码简单表述

Stack(
    children: [
      Positioned(///先右侧
        child: SingleChildScrollView(///此处用滚动view是因为要左右滑动
          child: Column(
            children: [
              Text("header"),
              ListView()
            ],
          ),
        ),
      ),
      Positioned(///再左侧
        child: Column(
          children: [
            Text("header"),
            ListView()
          ],
        ),
      )
    ],
  );

从代码看,现在已经可以实现表格的横向滚动了,只是左右表不会联动,即滚动左侧右侧不会动,滚动右侧左侧不会动。

那么作者是怎么实现互相联动的呢?

先看下对应的滚动控制器

  ScrollController _leftHandSideListViewScrollController = ScrollController();///左侧listview关联的滚动控制器
  ScrollController _rightHandSideListViewScrollController = ScrollController();///右侧listview关联的滚动控制器
  ScrollController _rightHorizontalScrollController = ScrollController();///右侧SingleChildScrollView关联的滚动控制器
  _SyncScrollControllerManager _syncScroller = _SyncScrollControllerManager();///控制左右两个listview联动的管理类
  ScrollShadowModel _scrollShadowModel = ScrollShadowModel();///保存左侧listview和右侧SingleChildScrollView滚动距离的model类

_SyncScrollControllerManager 控制左右两个listview联动的管理类

Widget _getScrollColumn(Widget child, ScrollController scrollController) {
    return NotificationListener(
      child: child,
      onNotification: (ScrollNotification scrollInfo) {
        _syncScroller.processNotification(scrollInfo, scrollController);
        return false;
      },
    );
  }

从代码看是在构造左右ListView的时候通过NotificationListener监听了两个ListView的滚动事件,然后做了处理来保持联动的。
那么关键代码就是processNotification这个方法,那就要先看_SyncScrollControllerManager类是做什么的。

class _SyncScrollControllerManager {
  List _registeredScrollControllers =
      new List();///用一个list来存储ScrollController

  ScrollController _scrollingController;///记录当前滚动的ScrollController
  bool _scrollingActive = false;///标记当前是否激活滚动
///添加ScrollController到list
  void registerScrollController(ScrollController controller) {
    _registeredScrollControllers.add(controller);
  }
///移除ScrollController从list中
  void unregisterScrollController(ScrollController controller) {
    _registeredScrollControllers.remove(controller);
  }
///处理滚动是多listView联动问题
  void processNotification(
      ScrollNotification notification, ScrollController sender) {
///当滚动开始,记录滚动的list,并激活滚动标志
    if (notification is ScrollStartNotification && !_scrollingActive) {
      _scrollingController = sender;
      _scrollingActive = true;
      return;
    }
///当记录的滚动listview和当前调用的是同一个listview,并且是激活状态
    if (identical(sender, _scrollingController) && _scrollingActive) {
///如果滚动结束就清除记录的滚动标志
      if (notification is ScrollEndNotification) {
        _scrollingController = null;
        _scrollingActive = false;
        return;
      }
///如果滚动更新,此处就是联动的关键,遍历list,找到不是当前滚动的listview,然后其偏移量和当前滚动的listview一致即实现联动
      if (notification is ScrollUpdateNotification) {
        _registeredScrollControllers.forEach((controller) {
          if (!identical(_scrollingController, controller)) {
            if (controller.hasClients) {
              controller.jumpTo(_scrollingController.offset);
            } else {}
          }
        });
        return;
      }
    }
  }
}

在init方法中可以看到把左右两个listview对应的ScrollController注册进了SyncScrollControllerManager中的list

void initState() {
    super.initState();
    _syncScroller
        .registerScrollController(_leftHandSideListViewScrollController);
    _syncScroller
        .registerScrollController(_rightHandSideListViewScrollController);
///以下两个监听作用下边讲
    _leftHandSideListViewScrollController.addListener(() {
      _scrollShadowModel.verticalOffset =
          _leftHandSideListViewScrollController.offset;
    });
    _rightHorizontalScrollController.addListener(() {
      _scrollShadowModel.horizontalOffset =
          _rightHorizontalScrollController.offset;
    });
  }

ScrollShadowModel _保存左侧listview和右侧SingleChildScrollView滚动距离的model类

看ScrollShadowModel的代码其实就是Provider的model类,保存一下变更的值,现在来看最上层的代码

return ChangeNotifierProvider(
        create: (context) => _scrollShadowModel,
        child: SafeArea(child: LayoutBuilder(
          builder: (context, boxConstraint) {
            return _getParallelListView(
                boxConstraint.maxWidth, boxConstraint.maxHeight);
          },
        )));

通过ChangeNotifierProvider把_scrollShadowModel共享给下层的widgets。这个作用是什么呢?
当你左右或者上下滚动表格的时候有没有发现左侧表格或者上部分表头会有阴影,如图


Flutter HorizontalDataTable源码解读_第3张图片
图片.png

Flutter HorizontalDataTable源码解读_第4张图片
图片.png

在看init方法中的这两个监听

_leftHandSideListViewScrollController.addListener(() {
      _scrollShadowModel.verticalOffset =
          _leftHandSideListViewScrollController.offset;
      setState(() {});
    });
    _rightHorizontalScrollController.addListener(() {
      _scrollShadowModel.horizontalOffset =
          _rightHorizontalScrollController.offset;
      setState(() {});
    });

监听了左侧表格和右侧横向SingleChildScrollView的滚动变化,然后把对应的offset赋值给_scrollShadowModel存储对应的垂直距离和水平距离,然后再下层用Selector监听对应的值,然后根据这个值的变化来显示不同程度的阴影。
这里不知道Selector的可以去搜一下provider中Selector的用法,是高级版的Consumer。

纳尼?Consumer也不知道?那也去搜呀!

那为什么没有监听右侧listview的滚动呢?
其实上边提到左右两个listview实现了滚动的联动,那么只要监听一个listview,另外一个listview也会滚动。

好像还有时间哎,那就再说说里边的布局吧。

有发现Stack布局的时候是先放的右侧listview,后放的左侧listview吗?
其实左右两侧都又通过Stack放了一个显示阴影的Container,Stack中先放的会在下层,那么左侧后放就会在右侧的上层,当显示左侧阴影的时候就会盖在右侧上方,如果相反的话右侧会盖着阴影,导致没有阴影效果的。

写在最后的感悟吧

其实很多时候我们(至少我)看完一些东西会感觉已经懂了,然后就过了,等过2天或者更久后再回过头来看又一脸懵逼,我看过吗?我当时懂了吗?我怎么没记录一下呢?
所以我也是经过很多次这样的一脸懵逼才狠下心来写了这篇文章,尽量把当时理解的记录下来,如果写的好,对你有帮助,那更好,如果很垃圾,请留言吐槽。当写这篇文章的途中,为了更好的解读,我又很详细的看了源码,然后写的过程中又会对之前的理解有一个更好的解读,这不就是另一种受益吗?

你可能感兴趣的:(Flutter HorizontalDataTable源码解读)