前言
今天看了Flutter Interact, 全程有個小姐姐翻譯(同聲翻譯,好強力),於是我就邊聽邊完成了這篇文章。
接著上一章Flutter Sliver一生之敵 (ExtendedList),這章我們將編寫一個瀑布流佈局,來檢驗一下我們上一章對Sliver列表原始碼分析是否正確。
歡迎加入Flutter Candies QQ群: 181398081。
- Flutter Sliver一生之敵 (ScrollView)
- Flutter Sliver一生之敵 (ExtendedList)
- Flutter Sliver你要的瀑布流小姐姐
- Flutter Sliver一生之敵 (LoadingMoreList)
原理
之前做UWP的時候,我自己也做過瀑布流佈局。似乎是一種執念,入坑Flutter之後也想實現一下瀑布流佈局。下面簡單講一下什麼是瀑布流以及原理。
瀑布流佈局的特點是等寬不等高。 為了讓最後一行的差距最小,從第二行開始,需要將一項放在第一行最矮的一項下面,以此類推,如下圖。4在0下面,5在3下面,6在1下面,7在2下面,8在4下面...
核心程式碼
知道了原理,下面我們來一起把原理實現為程式碼。
-
由於我們需要知道離viewport頂部最近的Items,以及viewport底部最近的Items。這樣才能知道向後滾動新的item放哪個item下面,或者說向前滾動的時候知道新的item放在哪個item的上面 我設計了CrossAxisItems 來存放leadingItems 和 trailingItems。
-
向後新增新的item的時候程式碼如下
1.補充leadingItems直到等於crossAxisCount
2.找到當前最矮的一項,將設定它的layoutoffset
3.儲存這一列的indexes
void insert({
@required RenderBox child,
@required ChildTrailingLayoutOffset childTrailingLayoutOffset,
@required PaintExtentOf paintExtentOf,
}) {
final WaterfallFlowParentData data = child.parentData;
final LastChildLayoutType lastChildLayoutType =
delegate.getLastChildLayoutType(data.index);
///處理最後一個特殊化佈局
switch (lastChildLayoutType) {
case LastChildLayoutType.fullCrossAxisExtend:
case LastChildLayoutType.foot:
//橫軸繪製offset
data.crossAxisOffset = 0.0;
//橫軸index
data.crossAxisIndex = 0;
//該child的大小
final size = paintExtentOf(child);
if (lastChildLayoutType == LastChildLayoutType.fullCrossAxisExtend ||
maxChildTrailingLayoutOffset + size >
constraints.remainingPaintExtent) {
data.layoutOffset = maxChildTrailingLayoutOffset;
} else {
//如果全部children沒有繪製viewport的大
data.layoutOffset = constraints.remainingPaintExtent - size;
}
data.trailingLayoutOffset = childTrailingLayoutOffset(child);
return;
case LastChildLayoutType.none:
break;
}
if (!leadingItems.contains(data)) {
//補充滿leadingItems
if (leadingItems.length != crossAxisCount) {
data.crossAxisIndex ??= leadingItems.length;
data.crossAxisOffset =
delegate.getCrossAxisOffset(constraints, data.crossAxisIndex);
if (data.index < crossAxisCount) {
data.layoutOffset = 0.0;
data.indexs.clear();
}
trailingItems.add(data);
leadingItems.add(data);
} else {
if (data.crossAxisIndex != null) {
var item = trailingItems.firstWhere(
(x) =>
x.index > data.index &&
x.crossAxisIndex == data.crossAxisIndex,
orElse: () => null);
///out of viewport
if (item != null) {
data.trailingLayoutOffset = childTrailingLayoutOffset(child);
return;
}
}
//找到最矮的那個
var min = trailingItems.reduce((curr, next) =>
((curr.trailingLayoutOffset < next.trailingLayoutOffset) ||
(curr.trailingLayoutOffset == next.trailingLayoutOffset &&
curr.crossAxisIndex < next.crossAxisIndex)
? curr
: next));
data.layoutOffset = min.trailingLayoutOffset + delegate.mainAxisSpacing;
data.crossAxisIndex = min.crossAxisIndex;
data.crossAxisOffset =
delegate.getCrossAxisOffset(constraints, data.crossAxisIndex);
trailingItems.forEach((f) => f.indexs.remove(min.index));
min.indexs.add(min.index);
data.indexs = min.indexs;
trailingItems.remove(min);
trailingItems.add(data);
}
}
data.trailingLayoutOffset = childTrailingLayoutOffset(child);
}
複製程式碼
- 向前新增新的item的時候程式碼如下
1.通過indexs找到新item屬於哪一列
2.新增到舊的item的上面
void insertLeading({
@required RenderBox child,
@required PaintExtentOf paintExtentOf,
}) {
final WaterfallFlowParentData data = child.parentData;
if (!leadingItems.contains(data)) {
var pre = leadingItems.firstWhere((x) => x.indexs.contains(data.index),
orElse: () => null);
if (pre == null || pre.index < data.index) return;
data.trailingLayoutOffset = pre.layoutOffset - delegate.mainAxisSpacing;
data.crossAxisIndex = pre.crossAxisIndex;
data.crossAxisOffset =
delegate.getCrossAxisOffset(constraints, data.crossAxisIndex);
leadingItems.remove(pre);
leadingItems.add(data);
trailingItems.remove(pre);
trailingItems.add(data);
data.indexs = pre.indexs;
data.layoutOffset = data.trailingLayoutOffset - paintExtentOf(child);
}
}
複製程式碼
- 計算離viewport頂部最近的,應該確保leadingItems都在viewport裡面 跟之前Listview的原始碼分析差不多,只是這裡我們要保證最大的LeadingLayoutOffset都小於scrollOffset,這樣leadingItems就都在viewport裡面了
if (crossAxisItems.maxLeadingLayoutOffset > scrollOffset) {
RenderBox child = firstChild;
//move to max index of leading
final int maxLeadingIndex = crossAxisItems.maxLeadingIndex;
while (child != null && maxLeadingIndex > indexOf(child)) {
child = childAfter(child);
}
//fill leadings from max index of leading to min index of leading
while (child != null && crossAxisItems.minLeadingIndex < indexOf(child)) {
crossAxisItems.insertLeading(
child: child, paintExtentOf: paintExtentOf);
child = childBefore(child);
}
//collectGarbage(maxLeadingIndex - index, 0);
while (crossAxisItems.maxLeadingLayoutOffset > scrollOffset) {
// We have to add children before the earliestUsefulChild.
earliestUsefulChild =
insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
if (earliestUsefulChild == null) {
if (scrollOffset == 0.0) {
// insertAndLayoutLeadingChild only lays out the children before
// firstChild. In this case, nothing has been laid out. We have
// to lay out firstChild manually.
firstChild.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = firstChild;
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
crossAxisItems.reset();
crossAxisItems.insert(
child: earliestUsefulChild,
childTrailingLayoutOffset: childTrailingLayoutOffset,
paintExtentOf: paintExtentOf,
);
break;
} else {
// We ran out of children before reaching the scroll offset.
// We must inform our parent that this sliver cannot fulfill
// its contract and that we need a scroll offset correction.
geometry = SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
);
return;
}
}
crossAxisItems.insertLeading(
child: earliestUsefulChild, paintExtentOf: paintExtentOf);
final WaterfallFlowParentData data = earliestUsefulChild.parentData;
// firstChildScrollOffset may contain double precision error
if (data.layoutOffset < -precisionErrorTolerance) {
// The first child doesn't fit within the viewport (underflow) and
// there may be additional children above it. Find the real first child
// and then correct the scroll position so that there's room for all and
// so that the trailing edge of the original firstChild appears where it
// was before the scroll offset correction.
// do this work incrementally, instead of all at once,
// i.e. find a way to avoid visiting ALL of the children whose offset
// is < 0 before returning for the scroll correction.
double correction = 0.0;
while (earliestUsefulChild != null) {
assert(firstChild == earliestUsefulChild);
correction += paintExtentOf(firstChild);
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints,
parentUsesSize: true);
crossAxisItems.insertLeading(
child: earliestUsefulChild, paintExtentOf: paintExtentOf);
}
geometry = SliverGeometry(
scrollOffsetCorrection: correction - data.layoutOffset,
);
return;
}
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
}
複製程式碼
- 計算達到viewport底部,應該確保trailingItems中最短的要超過viewport的底部
// Now find the first child that ends after our end.
if (child != null) {
while (crossAxisItems.minChildTrailingLayoutOffset <
targetEndScrollOffset ||
//make sure leading children are painted.
crossAxisItems.leadingItems.length < _gridDelegate.crossAxisCount
|| crossAxisItems.leadingItems.length > childCount
) {
if (!advance()) {
reachedEnd = true;
break;
}
}
}
複製程式碼
waterfall_flow使用
- 在pubspec.yaml中增加庫引用
dependencies:
waterfall_flow: any
複製程式碼
- 匯入庫
import 'package:waterfall_flow/waterfall_flow.dart';
複製程式碼
如何定義
你可以通過設定SliverWaterfallFlowDelegate引數來定義瀑布流
引數 | 描述 | 預設 |
---|---|---|
crossAxisCount | 橫軸的等長度元素數量 | 必填 |
mainAxisSpacing | 主軸元素之間的距離 | 0.0 |
crossAxisSpacing | 橫軸元素之間的距離 | 0.0 |
collectGarbage | 元素回收時候的回撥 | - |
lastChildLayoutTypeBuilder | 最後一個元素的佈局樣式(詳情請檢視後面) | - |
viewportBuilder | 可視區域中元素indexes變化時的回撥 | - |
closeToTrailing | 可否讓佈局緊貼trailing(詳情請檢視後面) | false |
WaterfallFlow.builder(
//cacheExtent: 0.0,
padding: EdgeInsets.all(5.0),
gridDelegate: SliverWaterfallFlowDelegate(
crossAxisCount: 2,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
/// follow max child trailing layout offset and layout with full cross axis extend
/// last child as loadmore item/no more item in [GridView] and [WaterfallFlow]
/// with full cross axis extend
// LastChildLayoutType.fullCrossAxisExtend,
/// as foot at trailing and layout with full cross axis extend
/// show no more item at trailing when children are not full of viewport
/// if children is full of viewport, it's the same as fullCrossAxisExtend
// LastChildLayoutType.foot,
lastChildLayoutTypeBuilder: (index) => index == _list.length
? LastChildLayoutType.foot
: LastChildLayoutType.none,
),
複製程式碼
結語
沒有再對原始碼有更多的分析,上一篇如果你看過了,應該會更加明白其中的道理。這一篇是對瀑布流原理在Flutter上面實現的展示,沒有做不到效果,只有想不到的效果,這就是Flutter給我帶來的體驗。
最後放上Flutter Interact 的一些內容,我是邊聽邊寫的,如果有誤,請提醒我更改下。
- Flutter 1.12版本
- Material design 以及字型庫
- 帶來了Desktop/Web(你現在可以嘗試他們了,不再是玩具)
- 叼炸天的各種開發工具(同時除錯7種裝置,UI化介面)
- gskinner 酷炫互動+開源
- supernova 知名的 design-to-code (設計轉程式碼) 工具
- Adobe XD 程式猿們顫抖吧,UI將代替你們了。
- flutter_vignettes商業互吹
- rive.app展示了一個炒雞可愛的遊戲,而且是用Flutter web
歡迎加入Flutter Candies,一起生產可愛的Flutter小糖果( QQ群:181398081)
最最後放上Flutter Candies全家桶,真香。