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)注釋,一看就知道怎么用了碾褂,那么我們開始看重點兽间。
框架
先了解一下整個框架,如圖
圖中紅框事整個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)左側表格或者上部分表頭會有陰影芭商,如圖
在看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)過很多次這樣的一臉懵逼才狠下心來寫了這篇文章妇蛀,盡量把當時理解的記錄下來耕突,如果寫的好,對你有幫助评架,那更好眷茁,如果很垃圾,請留言吐槽纵诞。當寫這篇文章的途中上祈,為了更好的解讀,我又很詳細的看了源碼浙芙,然后寫的過程中又會對之前的理解有一個更好的解讀登刺,這不就是另一種受益嗎?