Flutter HorizontalDataTable源码解读

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

看源码的初衷

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


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

正题

先看下构造方法吧

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');

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

框架

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


图片.png

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

Stack(
    children: <Widget>[
      Positioned(///先右侧
        child: SingleChildScrollView(///此处用滚动view是因为要左右滑动
          child: Column(
            children: <Widget>[
              Text("header"),
              ListView()
            ],
          ),
        ),
      ),
      Positioned(///再左侧
        child: Column(
          children: <Widget>[
            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<ScrollNotification>(
      child: child,
      onNotification: (ScrollNotification scrollInfo) {
        _syncScroller.processNotification(scrollInfo, scrollController);
        return false;
      },
    );
  }

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

class _SyncScrollControllerManager {
  List<ScrollController> _registeredScrollControllers =
      new List<ScrollController>();///用一个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<ScrollShadowModel>(
        create: (context) => _scrollShadowModel,
        child: SafeArea(child: LayoutBuilder(
          builder: (context, boxConstraint) {
            return _getParallelListView(
                boxConstraint.maxWidth, boxConstraint.maxHeight);
          },
        )));

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


图片.png

图片.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天或者更久后再回过头来看又一脸懵逼,我看过吗?我当时懂了吗?我怎么没记录一下呢?
所以我也是经过很多次这样的一脸懵逼才狠下心来写了这篇文章,尽量把当时理解的记录下来,如果写的好,对你有帮助,那更好,如果很垃圾,请留言吐槽。当写这篇文章的途中,为了更好的解读,我又很详细的看了源码,然后写的过程中又会对之前的理解有一个更好的解读,这不就是另一种受益吗?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351