不得不說,Flutter 繪製 UI 的速度和原生根本不是一個量級的,Flutter 要快的多了,好比經常使用的 ListView
控件,原生寫的話,好比 Android,若是不封裝的話,須要一個 Adapter
、ViewHolder
,再加個 xml 佈局文件,而 Flutter 可能就幾十行。緩存
對於越經常使用的控件,越要熟悉它的原理。Flutter 中的 ScrollView
家族,成員的劃分其實和 Android 仍是很是相似的,除了 ListView
、GridView
,還有 CustomScrollView
和 NestedScrollView
。今天咱們要講主角就是 ListView
。less
ListView
和 GridView
都繼承於 BoxScrollView
,但這裏並非繪製和佈局的地方,Flutter 和原生不太同樣,以 Android 爲例,Android 上繪製和佈局的單位是 View
和 ViewGroup
,Flutter 則要複雜一點,首先咱們用的最多的是各類 Widget
,好比 ListView
,但 Widget
能夠理解爲一個配置描述文件,好比如下代碼:ide
Container {
width: 100,
height: 100,
color: Colors.white,
}
複製代碼
這裏描述了咱們須要一個寬高爲 100,顏色爲白色的容器,最後真正去繪製的是 RenderObject
。而在 Widget
和 RenderObject
之間還有個 Element
,它的職責是,將咱們配置的 Widget Tree 轉換成 Element Tree,Element
是對 Widget
的進一步抽象,Element
有兩個子類,一個是 RenderObjectElement
,它持有 RenderObject
,還有一個 ComponentElement
,用於組合多個 RenderObjectElement
。這個是 Flutter UI 的核心,要理解好這三個類。佈局
回到咱們的主題上來,咱們前面說到 ListView
繼承於 BoxScrollView
,而 BoxScrollView
又繼承於 ScrollView
,ScrollView
是一個 StatelessWidget
,它依靠 Scrollable
實現滾動效果,而滾動容器中的 Widget
,稱爲 slivers。sliver 用於消費滾動事件。ui
@override
Widget build(BuildContext context) {
// slivers
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
// 省略
return primary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
}
複製代碼
BoxScrollView
實現了 buildSlivers()
,它只有一個 sliver,也就是滾動容器中,只有一個消費者。這裏又是經過調用 buildChildLayout
抽象方法建立。this
@override
List<Widget> buildSlivers(BuildContext context) {
// buildChildLayout
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry effectivePadding = padding;
// 省略
return <Widget>[ sliver ];
}
複製代碼
最後咱們的 ListView
就實現了 buildChildLayout()
:spa
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
// 若是子項是固定高度的
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
// 默認狀況
return SliverList(delegate: childrenDelegate);
}
複製代碼
SliverList
是一個 RenderObjectWidget
,上面咱們也說到了,最終繪製和佈局都是交與 RenderObject
去實現的。ListView
也不例外:code
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context;
return RenderSliverList(childManager: element);
}
複製代碼
RenderSliverList
是 ListView
的核心實現,也是咱們本文的重點。orm
有過 Android 自定義控件經驗的同窗會知道,當咱們自定義一個控件時,通常會涉及這幾個步驟:measure 和 draw,若是是自定義 ViewGroup
還會涉及 layout 過程,Flutter 也不例外,但它將 measure 和 layout 合併到 layout,draw 稱爲 paint。雖然叫法不同,但做用是同樣的。系統會調用 performLayout()
會執行測量和佈局,RenderSliverList
主要涉及佈局操做,因此咱們主要看下這個方法便可。cdn
performLayout()
代碼比較長,因此咱們會省略一些非核心代碼。
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
assert(scrollOffset >= 0.0);
final double remainingExtent = constraints.remainingCacheExtent;
assert(remainingExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingExtent;
final BoxConstraints childConstraints = constraints.asBoxConstraints();
複製代碼
scrollOffset
表示已滾動的偏移量,cacheOrigin
表示預佈局的相對位置。爲了更好的視覺效果,ListView
會在可見範圍內增長預佈局的區域,這裏表示下一次滾動即將展現的區域,稱爲 cacheExtent
。這個值能夠配置,默認爲 250。
// viewport.dart
double get cacheExtent => _cacheExtent;
double _cacheExtent;
set cacheExtent(double value) {
value = value ?? RenderAbstractViewport.defaultCacheExtent;
assert(value != null);
if (value == _cacheExtent)
return;
_cacheExtent = value;
markNeedsLayout();
}
static const double defaultCacheExtent = 250.0;
複製代碼
remainingCacheExtent
是當前該 sliver 可以使用的偏移量,這裏包含了預佈局的區域。這裏咱們用一張很是粗糙的圖片來解釋下。
C 區域表示咱們的屏幕,這裏咱們認爲是可見區域,實際狀況下,可能還要更小,由於 ListView
可能有些 padding
、magin
或者其餘佈局等。B 區域有兩個分別表示頭部的預佈局和底部的預佈局區域,它的值就是咱們設置的 cacheExtent
,A 區域回收區域。
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
複製代碼
這裏的 constraints.scrollOffset
就是 A + B,便可不見區域。constraints.cacheOrigin
在這裏,若是使用默認值,它等於 -250,意思就是說 B 的區域高度有 250,因此它徹底不可見時,它的相對位置 y 值就是 -250,這裏算出的 scrollOffset
其實就是開始佈局的起始位置,若是 cacheExtent = 0
,那麼它會從 C 的頂部開始佈局,即 constraints.scrollOffset
不然就是 constraints.scrollOffset + constraints.cacheOrigin
。
if (firstChild == null) {
// 若是沒有 children
if (!addInitialChild()) {
// There are no children.
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
// 至少存在一個 children
// leading 頭部,trailing 尾部
RenderBox leadingChildWithLayout, trailingChildWithLayout;
// Find the last child that is at or before the scrollOffset.
RenderBox earliestUsefulChild = firstChild;
for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild);
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
// 在頭部插入新的 children
earliestUsefulChild =
insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childParentData = firstChild
.parentData;
childParentData.layoutOffset = 0.0;
if (scrollOffset == 0.0) {
earliestUsefulChild = firstChild;
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
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;
}
}
final double firstChildScrollOffset = earliestScrollOffset -
paintExtentOf(firstChild);
if (firstChildScrollOffset < -precisionErrorTolerance) {
// 雙精度錯誤
}
final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild
.parentData;
// 更新 parentData
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
// 更新頭尾
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
複製代碼
上面的代碼是處理如下這種狀況,即 earliestScrollOffset > scrollOffset
,即頭部的 children 和 scrollOffset
之間有空間,沒有填充。畫個簡單的圖形。
這塊區域就是 needLayout。當從下向上滾動時候,就是這裏在進行佈局。
bool inLayoutRange = true;
RenderBox child = earliestUsefulChild;
int index = indexOf(child);
// endScrollOffset 表示當前已經佈局 children 的偏移量
double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
bool advance() {
assert(child != null);
if (child == trailingChildWithLayout)
inLayoutRange = false;
child = childAfter(child);
if (child == null)
inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child) != index) {
// 須要佈局新的 children,在尾部插入一個新的
child = insertAndLayoutChild(childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
if (child == null) {
// We have run out of children.
return false;
}
} else {
// Lay out the child.
child.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
// 更新 endScrollOffset,用當前 child 的偏移量 + child 所須要的範圍
endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
return true;
}
複製代碼
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
// 記錄須要回收的項目
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
// we want to make sure we keep the last child around so we know the end scroll offset
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent = childScrollOffset(lastChild) +
paintExtentOf(lastChild);
geometry = SliverGeometry(
scrollExtent: extent,
paintExtent: 0.0,
maxPaintExtent: extent,
);
return;
}
}
複製代碼
不在可見視圖,不在緩存區域的,記錄頭部須要回收的。
// Now find the first child that ends after our end.
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
複製代碼
從上往下滾動時,調用 advance()
不斷在底部插入新的 child。
// Finally count up all the remaining children and label them as garbage.
if (child != null) {
child = childAfter(child);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child);
}
}
// 回收
collectGarbage(leadingGarbage, trailingGarbage);
複製代碼
記錄尾部須要回收的,所有一塊兒回收。上圖中用 nedd grabage 標記的區域。
double estimatedMaxScrollOffset;
if (reachedEnd) {
// 沒有 child 須要佈局了
estimatedMaxScrollOffset = endScrollOffset;
} else {
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild),
lastIndex: indexOf(lastChild),
leadingScrollOffset: childScrollOffset(firstChild),
trailingScrollOffset: endScrollOffset,
);
assert(estimatedMaxScrollOffset >=
endScrollOffset - childScrollOffset(firstChild));
}
final double paintExtent = calculatePaintOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
final double cacheExtent = calculateCacheOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
final double targetEndScrollOffsetForPaint = constraints.scrollOffset +
constraints.remainingPaintExtent;
// 反饋佈局消費請求
geometry = SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint ||
constraints.scrollOffset > 0.0,
);
// 佈局結束
childManager.didFinishLayout();
複製代碼
在分析完 ListView
的佈局流程後,能夠發現整個流程仍是比較清晰的。