Flutter HorizontalDataTable源碼解讀

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

看源碼的初衷

這貨是個可以橫向滾動的表格,然后寫的也挺好的,但是這種表格數(shù)據(jù)都很大,不可能一次性加載完,所以就需要上拉加載功能。然后我就把pull_to_refresh嵌套著試試捣染,發(fā)現(xiàn)不行,只有拉表頭和表尾才能上拉加載,無奈就只能看源碼自己實現(xiàn)了芝加,先上圖看我自己實現(xiàn)的效果,雖然很丑射窒,但至少功能有了藏杖。


至于具體實現(xiàn)就不說了,比較丑陋脉顿,能用即可蝌麸,哦,對了艾疟,我是來源碼解讀了来吩,開始吧。

正題

先看下構造方法吧

const HorizontalDataTable({
    @required this.leftHandSideColumnWidth,///左側表格寬度
    @required this.rightHandSideColumnWidth,///右側表格寬度
    this.isFixedHeader = false,///是否顯示表格頭部
    this.headerWidgets,///表格頭部要顯示的widgets
    this.leftSideItemBuilder,///左側表格要顯示的widgetbuilder
    this.rightSideItemBuilder,///右側表格要顯示的widgetbuilder
    this.itemCount = 0,///表格行數(shù)
    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');

每個屬性的作用在備注里邊已經(jīng)注釋,一看就知道怎么用了碾褂,那么我們開始看重點兽间。

框架

先了解一下整個框架,如圖


圖片.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()
          ],
        ),
      )
    ],
  );

從代碼看帜羊,現(xiàn)在已經(jīng)可以實現(xiàn)表格的橫向滾動了,只是左右表不會聯(lián)動鸠天,即滾動左側右側不會動讼育,滾動右側左側不會動。

那么作者是怎么實現(xiàn)互相聯(lián)動的呢稠集?

先看下對應的滾動控制器

  ScrollController _leftHandSideListViewScrollController = ScrollController();///左側listview關聯(lián)的滾動控制器
  ScrollController _rightHandSideListViewScrollController = ScrollController();///右側listview關聯(lián)的滾動控制器
  ScrollController _rightHorizontalScrollController = ScrollController();///右側SingleChildScrollView關聯(lián)的滾動控制器
  _SyncScrollControllerManager _syncScroller = _SyncScrollControllerManager();///控制左右兩個listview聯(lián)動的管理類
  ScrollShadowModel _scrollShadowModel = ScrollShadowModel();///保存左側listview和右側SingleChildScrollView滾動距離的model類

_SyncScrollControllerManager 控制左右兩個listview聯(lián)動的管理類

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

從代碼看是在構造左右ListView的時候通過NotificationListener監(jiān)聽了兩個ListView的滾動事件奶段,然后做了處理來保持聯(lián)動的。
那么關鍵代碼就是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);
  }
///處理滾動是多l(xiāng)istView聯(lián)動問題
  void processNotification(
      ScrollNotification notification, ScrollController sender) {
///當滾動開始,記錄滾動的list晦鞋,并激活滾動標志
    if (notification is ScrollStartNotification && !_scrollingActive) {
      _scrollingController = sender;
      _scrollingActive = true;
      return;
    }
///當記錄的滾動listview和當前調(diào)用的是同一個listview蹲缠,并且是激活狀態(tài)
    if (identical(sender, _scrollingController) && _scrollingActive) {
///如果滾動結束就清除記錄的滾動標志
      if (notification is ScrollEndNotification) {
        _scrollingController = null;
        _scrollingActive = false;
        return;
      }
///如果滾動更新棺克,此處就是聯(lián)動的關鍵,遍歷list线定,找到不是當前滾動的listview娜谊,然后其偏移量和當前滾動的listview一致即實現(xiàn)聯(lián)動
      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);
///以下兩個監(jiān)聽作用下邊講
    _leftHandSideListViewScrollController.addListener(() {
      _scrollShadowModel.verticalOffset =
          _leftHandSideListViewScrollController.offset;
    });
    _rightHorizontalScrollController.addListener(() {
      _scrollShadowModel.horizontalOffset =
          _rightHorizontalScrollController.offset;
    });
  }

ScrollShadowModel _保存左側listview和右側SingleChildScrollView滾動距離的model類

看ScrollShadowModel的代碼其實就是Provider的model類,保存一下變更的值斤讥,現(xiàn)在來看最上層的代碼

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

通過ChangeNotifierProvider把_scrollShadowModel共享給下層的widgets纱皆。這個作用是什么呢?
當你左右或者上下滾動表格的時候有沒有發(fā)現(xiàn)左側表格或者上部分表頭會有陰影芭商,如圖


圖片.png

圖片.png

在看init方法中的這兩個監(jiān)聽

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

監(jiān)聽了左側表格和右側橫向SingleChildScrollView的滾動變化抹剩,然后把對應的offset賦值給_scrollShadowModel存儲對應的垂直距離和水平距離,然后再下層用Selector監(jiān)聽對應的值蓉坎,然后根據(jù)這個值的變化來顯示不同程度的陰影。
這里不知道Selector的可以去搜一下provider中Selector的用法胡嘿,是高級版的Consumer蛉艾。

納尼?Consumer也不知道衷敌?那也去搜呀勿侯!

那為什么沒有監(jiān)聽右側listview的滾動呢?
其實上邊提到左右兩個listview實現(xiàn)了滾動的聯(lián)動缴罗,那么只要監(jiān)聽一個listview助琐,另外一個listview也會滾動。

好像還有時間哎面氓,那就再說說里邊的布局吧兵钮。

有發(fā)現(xiàn)Stack布局的時候是先放的右側listview,后放的左側listview嗎舌界?
其實左右兩側都又通過Stack放了一個顯示陰影的Container掘譬,Stack中先放的會在下層,那么左側后放就會在右側的上層呻拌,當顯示左側陰影的時候就會蓋在右側上方葱轩,如果相反的話右側會蓋著陰影,導致沒有陰影效果的藐握。

寫在最后的感悟吧

其實很多時候我們(至少我)看完一些東西會感覺已經(jīng)懂了靴拱,然后就過了,等過2天或者更久后再回過頭來看又一臉懵逼猾普,我看過嗎袜炕?我當時懂了嗎?我怎么沒記錄一下呢抬闷?
所以我也是經(jīng)過很多次這樣的一臉懵逼才狠下心來寫了這篇文章妇蛀,盡量把當時理解的記錄下來耕突,如果寫的好,對你有幫助评架,那更好眷茁,如果很垃圾,請留言吐槽纵诞。當寫這篇文章的途中上祈,為了更好的解讀,我又很詳細的看了源碼浙芙,然后寫的過程中又會對之前的理解有一個更好的解讀登刺,這不就是另一種受益嗎?

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嗡呼,一起剝皮案震驚了整個濱河市纸俭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌南窗,老刑警劉巖揍很,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異万伤,居然都是意外死亡窒悔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門敌买,熙熙樓的掌柜王于貴愁眉苦臉地迎上來简珠,“玉大人,你說我怎么就攤上這事虹钮×郑” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵芙粱,是天一觀的道長珍策。 經(jīng)常有香客問我,道長宅倒,這世上最難降的妖魔是什么攘宙? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮拐迁,結果婚禮上蹭劈,老公的妹妹穿的比我還像新娘。我一直安慰自己线召,他們只是感情好铺韧,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缓淹,像睡著了一般哈打。 火紅的嫁衣襯著肌膚如雪塔逃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天料仗,我揣著相機與錄音湾盗,去河邊找鬼。 笑死立轧,一個胖子當著我的面吹牛格粪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播氛改,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼帐萎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胜卤?” 一聲冷哼從身側響起疆导,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎葛躏,沒想到半個月后是鬼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡紫新,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了李剖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芒率。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖篙顺,靈堂內(nèi)的尸體忽然破棺而出偶芍,到底是詐尸還是另有隱情,我是刑警寧澤德玫,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布匪蟀,位于F島的核電站,受9級特大地震影響宰僧,放射性物質發(fā)生泄漏材彪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一琴儿、第九天 我趴在偏房一處隱蔽的房頂上張望段化。 院中可真熱鬧券躁,春花似錦煌茬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喘蟆。三九已至缓升,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蕴轨,已是汗流浹背港谊。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尺棋,地道東北人封锉。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像膘螟,于是被迫代替她去往敵國和親成福。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355