接着上一章Flutter Sliver一輩子之敵 (ScrollView),咱們這章將沿着ListView/GridView => SliverList/SliverGrid => RenderSliverList/RenderSliverGrid的感情線,梳理列表計算的最終一千米代碼,舉一反N。ios
歡迎加入Flutter Candies QQ羣: 181398081。git
在講解佈局代碼以前,先要了解下Sliver佈局的輸入和輸出github
Sliver佈局的輸入,就是Viewport告訴咱們的約束。面試
class SliverConstraints extends Constraints {
/// Creates sliver constraints with the given information.
///
/// All of the argument must not be null.
const SliverConstraints({
//滾動的方向
@required this.axisDirection,
//這個是給center使用的,center以前的sliver是顛倒的
@required this.growthDirection,
//用戶手勢的方向
@required this.userScrollDirection,
//滾動的偏移量,注意這裏是針對這個Sliver的,並且非整個Slivers的總滾動偏移量
@required this.scrollOffset,
//前面Slivers的總的大小
@required this.precedingScrollExtent,
//爲pinned和floating設計的,若是前一個Sliver繪製大小爲100,可是佈局大小隻有50,那麼這個Sliver的overlap爲50.
@required this.overlap,
//還有多少內容能夠繪製,參考viewport以及cache。好比多Slivers的時候,前一個佔了100,那麼後面能繪製的區域就要減掉前面繪製的區域大小,獲得剩餘的繪製區域大小
@required this.remainingPaintExtent,
//縱軸的大小
@required this.crossAxisExtent,
//縱軸的方向,這裏會影響GridView同一行元素的擺放順序,是0~x,仍是x~0
@required this.crossAxisDirection,
//viewport中還有多少內容能夠繪製
@required this.viewportMainAxisExtent,
//剩餘的緩存區域大小
@required this.remainingCacheExtent,
//相對於scrollOffset緩存區域大小
@required this.cacheOrigin,
})
複製代碼
Sliver佈局的輸出,將會反饋給Viewport。api
@immutable
class SliverGeometry extends Diagnosticable {
/// Creates an object that describes the amount of space occupied by a sliver.
///
/// If the [layoutExtent] argument is null, [layoutExtent] defaults to the
/// [paintExtent]. If the [hitTestExtent] argument is null, [hitTestExtent]
/// defaults to the [paintExtent]. If [visible] is null, [visible] defaults to
/// whether [paintExtent] is greater than zero.
///
/// The other arguments must not be null.
const SliverGeometry({
//預估的Sliver可以滾動大小
this.scrollExtent = 0.0,
//對後一個的overlap屬性有影響,它小於[SliverConstraints.remainingPaintExtent],爲Sliver在viewport範圍(包含cache)內第一個元素到最後一個元素的大小
this.paintExtent = 0.0,
//相對Sliver位置的繪製起點
this.paintOrigin = 0.0,
//這個sliver在viewport的第一個顯示位置到下一個sliver的第一個顯示位置的大小
double layoutExtent,
//最大能繪製的總大小,這個參數是用於[SliverConstraints.remainingPaintExtent] 是無窮大的,就是使用在shrink-wrapping viewport中
this.maxPaintExtent = 0.0,
//若是sliver被pinned在邊界的時候,這個大小爲Sliver的自身的高度。其餘狀況爲0
this.maxScrollObstructionExtent = 0.0,
//點擊有效區域的大小,默認爲paintExtent
double hitTestExtent,
//可見,paintExtent爲0不可見。
bool visible,
//是否須要作clip,省得chidren溢出
this.hasVisualOverflow = false,
//viewport layout sliver的時候,若是sliver出現了一些問題,那麼這個值將不等於0,經過這個值來修正整個滾動的ScrollOffset
this.scrollOffsetCorrection,
//該Sliver使用了多少[SliverConstraints.remainingCacheExtent],針對多Slivers的狀況
double cacheExtent,
})
複製代碼
大概講解了這些參數的意義,可能仍是不太明白,在後面的源碼中使用中還會根據場景進行講解。瀏覽器
Widget | Extends |
---|---|
ListView/GridView | BoxScrollView => ScrollView |
ListView 和 GirdView 都繼承與BoxScrollView,咱們先看看BoxScrollView跟ScrollView有什麼區別。緩存
關鍵代碼bash
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry padding;
@override
List<Widget> buildSlivers(BuildContext context) {
/// 這個方法被ListView/GirdView 實現
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry effectivePadding = padding;
if (padding == null) {
final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
if (mediaQuery != null) {
// Automatically pad sliver with padding from MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
if (effectivePadding != null)
sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
return <Widget>[ sliver ];
}
/// Subclasses should override this method to build the layout model.
@protected
/// 這個方法被ListView/GirdView 實現
Widget buildChildLayout(BuildContext context);
複製代碼
能夠看出來,只是多包了一層SliverPadding,最後返回的[ sliver ]也說明,其實ListView和GridView 跟CustomScrollView相比,前者是單個Sliver,後者可爲多個Slivers.markdown
關鍵代碼app
在BoxScrollView的buildSlivers方法中調用了buildChildLayout,下面是在ListView中的實現。能夠看到根據itemExtent來分別返回了SliverList和SliverFixedExtentList 2種Sliver。
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
return SliverList(delegate: childrenDelegate);
}
複製代碼
class SliverList extends SliverMultiBoxAdaptorWidget {
/// Creates a sliver that places box children in a linear array.
const SliverList({
Key key,
@required SliverChildDelegate delegate,
}) : super(key: key, delegate: delegate);
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context;
return RenderSliverList(childManager: element);
}
}
複製代碼
RenderSliverList中的performLayout (github.com/flutter/flu…)方法是用於佈局children,在講解代碼以前咱們先看一下單個Sliver的children佈局的狀況。
圖中綠色的爲咱們能看到的部分,黃色是緩存區域,灰色爲應該回收掉的部分。
//指示開始
childManager.didStartLayout();
//指示是否能夠添加新的child
childManager.setDidUnderflow(false);
//constraints就是viewport給咱們的佈局限制,也就是佈局輸入
//滾動位置包含cache,佈局區域開始位置
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;
//獲取到child的限制,若是是垂直滾動的列表,高度應該是無限大double.infinity
final BoxConstraints childConstraints = constraints.asBoxConstraints();
//從第一個child開始向後須要回收的孩子個數,圖中灰色部分
int leadingGarbage = 0;
//從最後一個child開始向前須要回收的孩子個數,圖中灰色部分
int trailingGarbage = 0;
//是否滾動到最後
bool reachedEnd = false;
//若是列表裏面沒有一個child,咱們將嘗試加入一個,若是加入失敗,那麼整個Sliver無內容
if (firstChild == null) {
if (!addInitialChild()) {
// There are no children.
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
複製代碼
// Find the last child that is at or before the scrollOffset.
RenderBox earliestUsefulChild = firstChild;
//當第一個child的layoutOffset小於咱們的滾動位置的時候,說明前面是空的,若是在第一個child的簽名插入一個新的child來填充
for (double earliestScrollOffset =
childScrollOffset(earliestUsefulChild);
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
// We have to add children before the earliestUsefulChild.
// 這裏就是在插入新的child
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
//處理當前面已經沒有child的時候
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
//已經到0.0的位置了,因此不須要再向前找了,break
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;
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.
// 這裏就是咱們上一章講的,出現出錯了。將scrollOffsetCorrection設置爲不爲0,傳遞給viewport,這樣它會總體從新移除掉這個差值,從新進行layout佈局。
geometry = SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
);
return;
}
}
/// 滾動的位置減掉firstChild的大小,用來繼續計算是否還須要插入更多child來補足前面。
final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild);
// firstChildScrollOffset may contain double precision error
// 一樣的道理,若是發現最終減掉以後,數值小於0.0(precisionErrorTolerance這是一個接近0.0的極小數)的話,確定是不對的,因此又告訴viewport移除掉差值,從新佈局
if (firstChildScrollOffset < -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.
// TODO(hansmuller): 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);
}
geometry = SliverGeometry(
scrollOffsetCorrection: correction - earliestScrollOffset,
);
final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}
// ok,這裏就是正常的狀況
final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData as SliverMultiBoxAdaptorParentData;
// 設置child繪製的開始點
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
複製代碼
向後移動child,若是沒有了返回false
bool inLayoutRange = true;
RenderBox child = earliestUsefulChild;
int index = indexOf(child);
double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
bool advance() { // returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication.
assert(child != null);
if (child == trailingChildWithLayout)
inLayoutRange = false;
child = childAfter(child);
///不在render tree裏面
if (child == null)
inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child) != index) {
// We are missing a child. Insert it (and lay it out) if possible.
//不在樹裏面,嘗試新增進去
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 as SliverMultiBoxAdaptorParentData;
//設置繪製位置
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
//設置endScrollOffset爲child的繪製結束位置
endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
return true;
}
複製代碼
當向後滾動的時候,第一個child也許不是離scrollOffset最近的,因此咱們須要向後找,找到這個最近的。
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
//若是是小於,說明須要被回收,這裏+1記錄一下。
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
//找到最後都沒有知足的話,將以最後一個child爲準
// 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;
}
}
// Finally count up all the remaining children and label them as garbage.
//到上面位置是須要佈局的最後一個child,因此在它以後的child就是須要被回收的
if (child != null) {
child = childAfter(child);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child);
}
}
複製代碼
// At this point everything should be good to go, we just have to clean up
// the garbage and report the geometry.
// 使用以前計算出來的回收參數
collectGarbage(leadingGarbage, trailingGarbage);
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
assert(_debugAssertChildListLocked());
assert(childCount >= leadingGarbage + trailingGarbage);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
//從第一個向後刪除
while (leadingGarbage > 0) {
_destroyOrCacheChild(firstChild);
leadingGarbage -= 1;
}
//從最後一個向前刪除
while (trailingGarbage > 0) {
_destroyOrCacheChild(lastChild);
trailingGarbage -= 1;
}
// Ask the child manager to remove the children that are no longer being
// kept alive. (This should cause _keepAliveBucket to change, so we have
// to prepare our list ahead of time.)
_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
return !childParentData.keepAlive;
}).toList().forEach(_childManager.removeChild);
assert(_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
return !childParentData.keepAlive;
}).isEmpty);
});
}
void _destroyOrCacheChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
//若是child被標記爲緩存的話,從tree中移除而且放入緩存中
if (childParentData.keepAlive) {
assert(!childParentData._keptAlive);
remove(child);
_keepAliveBucket[childParentData.index] = child;
child.parentData = childParentData;
super.adoptChild(child);
childParentData._keptAlive = true;
} else {
assert(child.parent == this);
//直接移除
_childManager.removeChild(child);
assert(child.parent == null);
}
}
複製代碼
assert(debugAssertChildListIsNonEmptyAndContiguous());
double estimatedMaxScrollOffset;
//以及到底了,直接使用最後一個child的繪製結束位置
if (reachedEnd) {
estimatedMaxScrollOffset = endScrollOffset;
} else {
// 計算出估計最大值
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild),
lastIndex: indexOf(lastChild),
leadingScrollOffset: childScrollOffset(firstChild),
trailingScrollOffset: endScrollOffset,
);
assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
}
//根據remainingPaintExtent算出當前消耗了的繪製區域大小
final double paintExtent = calculatePaintOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
//根據remainingCacheExtent算出當前消耗了的緩存繪製區域大小
final double cacheExtent = calculateCacheOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
//佈局區域結束位置
final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
//將輸出反饋給Viewport,viewport根據sliver的輸出,若是這個sliver已經沒有內容了,再佈局下一個
geometry = SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
//是否須要clip
hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
);
// We may have started the layout while scrolled to the end, which would not
// expose a new child.
// 2者相等說明已經這個sliver的底部了
if (estimatedMaxScrollOffset == endScrollOffset)
childManager.setDidUnderflow(true);
//通知完成layout
//這裏會經過[SliverChildDelegate.didFinishLayout] 將第一個index和最後一個index傳遞出去,能夠用追蹤
childManager.didFinishLayout();
複製代碼
static double _extrapolateMaxScrollOffset(
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
int childCount,
) {
if (lastIndex == childCount - 1)
return trailingScrollOffset;
final int reifiedCount = lastIndex - firstIndex + 1;
//算出平均值
final double averageExtent = (trailingScrollOffset - leadingScrollOffset) / reifiedCount;
//加上剩餘估計值
final int remainingCount = childCount - lastIndex - 1;
return trailingScrollOffset + averageExtent * remainingCount;
}
複製代碼
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
// offset is to the top-left corner, regardless of our axis direction.
// originOffset gives us the delta from the real origin to the origin in the axis direction.
Offset mainAxisUnit, crossAxisUnit, originOffset;
bool addExtent;
// 根據滾動的方向,來獲取主軸和橫軸的係數
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
mainAxisUnit = const Offset(0.0, -1.0);
crossAxisUnit = const Offset(1.0, 0.0);
originOffset = offset + Offset(0.0, geometry.paintExtent);
addExtent = true;
break;
case AxisDirection.right:
mainAxisUnit = const Offset(1.0, 0.0);
crossAxisUnit = const Offset(0.0, 1.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.down:
mainAxisUnit = const Offset(0.0, 1.0);
crossAxisUnit = const Offset(1.0, 0.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.left:
mainAxisUnit = const Offset(-1.0, 0.0);
crossAxisUnit = const Offset(0.0, 1.0);
originOffset = offset + Offset(geometry.paintExtent, 0.0);
addExtent = true;
break;
}
assert(mainAxisUnit != null);
assert(addExtent != null);
RenderBox child = firstChild;
while (child != null) {
//獲取child主軸的位置,爲child的layoutOffset減去滾動位移scrollOffset
final double mainAxisDelta = childMainAxisPosition(child);
//獲取child橫軸的位置,ListView爲0.0, GridView爲計算出來的crossAxisOffset
final double crossAxisDelta = childCrossAxisPosition(child);
Offset childOffset = Offset(
originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta,
originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta,
);
if (addExtent)
childOffset += mainAxisUnit * paintExtentOf(child);
// If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child))
// does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden.
// 這裏能夠看到由於有cache的緣由,有一些child實際上是不須要繪製在咱們能夠看到的可視區域的
if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0)
context.paintChild(child, childOffset);
child = childAfter(child);
}
}
複製代碼
當ListView的itemExtent不爲null的時候,使用的是RenderSliverFixedExtentList。這個咱們也只簡單講一下,因爲知道了child主軸的高度,再各類計算當中就更加簡單。咱們能夠根據scrollOffset和viewport直接算出來第一個child和最後一個child。
最後是咱們的GridView,由於GridView的設計爲child的主軸大小和橫軸大小/橫軸child個數相等(固然還跟childAspectRatio(默認爲1.0)寬高比例有關係),因此說其實child主軸的大小也是已知的,而橫軸的繪製位置也很好定.基本上的計算原理也跟ListView差很少了。
講了一堆源碼,不知道有多少人能看到這裏。咱們經過對源碼分析,知道了sliver列表的一些計算繪製知識。接下來咱們將對官方的Sliver 列表作一些擴展,來知足羞羞的效果。
常常聽到有小夥伴說圖片列表滾動幾下就閃退,這種狀況在ios上面特別明顯,而在安卓上面內存增加的很快,其緣由是Flutter默認爲圖片作了內存緩存。就是說你若是滾動列表加載了300張圖片,那麼內存裏面就會有300張圖片的內存緩存,官方緩存上限爲1000.
首先,咱們來看看不作任何處理的狀況下,圖片列表的內存。我在這裏作了一個圖片列表,常見的9宮格的圖片列表,增量加載child的總個數爲300個,也就是說加載完畢以後可能有(1~9)*300=(300~2700)個圖片內存緩存,固然由於官方緩存爲1000,最終圖片內存緩存應該在300到1000之間(若是總的圖片大小沒有超過官方的限制)。
flutter packages pub global activate devtools
激活 dart devtools
flutter --no-color packages pub global run devtools --machine --port=0
將上圖中的 127.0.0.1:9540 地址輸入到瀏覽器中。flutter run --profile
運行起來咱們的測試應用
執行完畢以後,會有一個地址,咱們將這個地址copy到devtools中的Connect
上面例子很明顯看到多圖片列表對內存的巨大消耗,咱們前面瞭解了Flutter中列表繪製整個流程,那麼咱們有沒有辦法來改進一下內存呢? 答案是咱們能夠嘗試在列表children回收的時候,咱們主動去清除掉那個child中包含圖片的內存緩存。這樣內存中只有咱們列表中少許的圖片內存,另外一方面因爲咱們圖片作了硬盤緩存,即便咱們清除了內存緩存,圖片從新加載的時候也不會再次下載,對於用戶來講無感知的。
咱們前面提到過官方的collectGarbage方法,這個方法調用的時候將去清除掉不須要的children。那麼咱們能夠在這個時刻將被清除children的indexes獲取到而且通知用戶。
關鍵代碼以下。因爲我不想重寫更多的Sliver底層的類,因此我這裏是經過ExtendedListDelegate中的回調將indexes傳遞出來。
void callCollectGarbage({
CollectGarbage collectGarbage,
int leadingGarbage,
int trailingGarbage,
int firstIndex,
int targetLastIndex,
}) {
if (collectGarbage == null) return;
List<int> garbages = [];
firstIndex ??= indexOf(firstChild);
targetLastIndex ??= indexOf(lastChild);
for (var i = leadingGarbage; i > 0; i--) {
garbages.add(firstIndex - i);
}
for (var i = 0; i < trailingGarbage; i++) {
garbages.add(targetLastIndex + i);
}
if (garbages.length != 0) {
//call collectGarbage
collectGarbage.call(garbages);
}
}
複製代碼
當通知chilren被清除的時候,經過ImageProvider.evict方法將圖片緩存從內存中移除掉。
SliverListConfig<TuChongItem>(
collectGarbage: (List<int> indexes) {
///collectGarbage
indexes.forEach((index) {
final item = listSourceRepository[index];
if (item.hasImage) {
item.images.forEach((image) {
final provider = ExtendedNetworkImageProvider(
image.imageUrl,
);
provider.evict();
});
}
});
},
複製代碼
通過優化以後執行一樣的步驟,安卓內存變化爲下
ios也差很少,表現爲下
從上面測試中,咱們能夠看到通過優化,圖片列表的內存獲得了大大的優化,基本知足咱們的需求。可是咱們作的還不夠極限,由於對於列表圖片來講,一般咱們對它的圖片質量其實不是那麼高的(我又想起來了列表圖片一張8m的那個大哥)
固然這種用法的前提是你已經提早知道了圖片的大小,這樣你能夠對圖片進行等比壓縮。好比下面代碼我對寬高進行了5倍縮小。注意的是,這樣作了以後,圖片的質量將會降低,若是過小了,就會糊掉。請根據本身的狀況進行設置。另一個問題是,列表圖片和點擊圖片進行預覽的圖片,由於不是同一個ImageProvider了(預覽圖片通常都但願是高清的),因此會重複下載,請根據本身的狀況進行取捨。
ImageProvider createResizeImage() {
return ResizeImage(ExtendedNetworkImageProvider(imageUrl),
width: width ~/ 5, height: height ~/ 5);
}
複製代碼
///override this method, so that you can handle raw image data,
///for example, compress
Future<ui.Codec> instantiateImageCodec(
Uint8List data, DecoderCallback decode) async {
_rawImageData = data;
return await decode(data);
}
複製代碼
若是方案對你有用,請支持一下我對collectGarbage的PR.
add collectGarbage method for SliverChildDelegate to track which children can be garbage collected
這樣可讓更多人解決掉圖片列表內存的問題。固然你也能夠直接使用 ExtendedList WaterfallFlow 和 LoadingMoreList 它們都支持這個api。整個完整的解決方案我已經提交到了ExtendedImage的demo當中,方便查看整個流程。
簡單的說,就是咱們怎麼方便地知道在可視區域中的children呢?從列表的計算繪製過程當中,其實咱們是可以輕易獲取到可視區域中children的indexes的。我這裏提供了ViewportBuilder回調來獲取可視區域中第一個index和最後一個index。 代碼位置
一樣是經過ExtendedListDelegate,在viewportBuilder中回調。
使用演示
ExtendedListView.builder(
extendedListDelegate: ExtendedListDelegate(
viewportBuilder: (int firstIndex, int lastIndex) {
print("viewport : [$firstIndex,$lastIndex]");
}),
複製代碼
咱們在入門Flutter的時候,作增量加載列表的時候,看到的例子就是把最後一個child做爲loadmore/no more。ListView若是滿屏幕的時候沒有什麼問題,可是下面狀況須要解決。
爲了解決這個問題,我設計了lastChildLayoutTypeBuilder。經過用戶告訴的最後一個child的類型,來佈局最後一個child,下面以RenderSliverList爲例子。
if (reachedEnd) {
///zmt
final layoutType = extendedListDelegate?.lastChildLayoutTypeBuilder
?.call(indexOf(lastChild)) ??
LastChildLayoutType.none;
// 最後一個child的大小
final size = paintExtentOf(lastChild);
// 最後一個child 繪製的結束位置
final trailingLayoutOffset = childScrollOffset(lastChild) + size;
//若是最後一個child繪製的結束位置小於了剩餘繪製大小,那麼咱們將最後一個child的位置改成constraints.remainingPaintExtent - size
if (layoutType == LastChildLayoutType.foot &&
trailingLayoutOffset < constraints.remainingPaintExtent) {
final SliverMultiBoxAdaptorParentData childParentData =
lastChild.parentData;
childParentData.layoutOffset = constraints.remainingPaintExtent - size;
endScrollOffset = constraints.remainingPaintExtent;
}
estimatedMaxScrollOffset = endScrollOffset;
}
複製代碼
最後咱們看看怎麼使用。
enum LastChildLayoutType {
/// 普通的
none,
/// 將最後一個元素繪製在最大主軸Item以後,而且使用橫軸大小最爲layout size
/// 主要使用在[ExtendedGridView] and [WaterfallFlow]中,最後一個元素做爲loadmore/no more元素的時候。
fullCrossAxisExtend,
/// 將最後一個child繪製在trailing of viewport,而且使用橫軸大小最爲layout size
/// 這種經常使用於最後一個元素做爲loadmore/no more元素,而且列表元素沒有充滿整個viewport的時候
/// 若是列表元素充滿viewport,那麼效果跟fullCrossAxisExtend同樣
foot,
}
ExtendedListView.builder(
extendedListDelegate: ExtendedListDelegate(
// 列表的總長度應該是 length + 1
lastChildLayoutTypeBuilder: (index) => index == length
? LastChildLayoutType.foot
: LastChildLayoutType.none,
),
複製代碼
咱們在作一個聊天列表的時候,由於佈局是從上向下的,咱們第一反應確定是將 ListView的reverse設置爲true,當有新的會話會被插入0的位置,這樣設置是最簡單,可是當會話沒有充滿viewport的時候,由於佈局被翻轉,因此佈局會像下面這樣。
trailing
-----------------
| |
| |
| item2 |
| item1 |
| item0 |
-----------------
leading
複製代碼
爲了解決這個問題,你能夠設置 closeToTrailing 爲true, 佈局將變成以下 該屬性同時支持[ExtendedGridView],[ExtendedList],[WaterfallFlow]。 固然若是reverse若是不爲ture,你設置這個屬性依然會生效,沒滿viewport的時候佈局會緊靠trailing。
trailing
-----------------
| item2 |
| item1 |
| item0 |
| |
| |
-----------------
leading
複製代碼
那是如何是現實的呢?爲此我增長了2個擴展方法
若是最後一個child的繪製結束位置沒有剩餘繪製區域大(也就是children未填充滿viewport),那麼咱們給每個child的繪製起點增長constraints.remainingPaintExtent - endScrollOffset的距離,那麼現象就會是所有children是緊靠trailing佈局的。這個方法爲總體計算佈局以後調用。
/// handle closeToTrailing at end
double handleCloseToTrailingEnd(
bool closeToTrailing, double endScrollOffset) {
if (closeToTrailing && endScrollOffset < constraints.remainingPaintExtent) {
RenderBox child = firstChild;
final distance = constraints.remainingPaintExtent - endScrollOffset;
while (child != null) {
final SliverMultiBoxAdaptorParentData childParentData =
child.parentData;
childParentData.layoutOffset += distance;
child = childAfter(child);
}
return constraints.remainingPaintExtent;
}
return endScrollOffset;
}
複製代碼
由於咱們給每一個child的繪製起點增長了constraints.remainingPaintExtent - endScrollOffset的距離。再下一次performLayout的時候,咱們應該先移除掉這部分的距離。當第一個child的index爲0 而且layoutOffset不爲0,咱們須要將所有的children的layoutOffset作移除。
/// handle closeToTrailing at begin
void handleCloseToTrailingBegin(bool closeToTrailing) {
if (closeToTrailing) {
RenderBox child = firstChild;
SliverMultiBoxAdaptorParentData childParentData = child.parentData;
// 所有移除掉前一次performLayout增長的距離
if (childParentData.index == 0 && childParentData.layoutOffset != 0) {
var distance = childParentData.layoutOffset;
while (child != null) {
childParentData = child.parentData;
childParentData.layoutOffset -= distance;
child = childAfter(child);
}
}
}
}
複製代碼
最後咱們看看怎麼使用。
ExtendedListView.builder(
reverse: true,
extendedListDelegate: ExtendedListDelegate(closeToTrailing: true),
複製代碼
這一章咱們經過對sliver 列表的源碼進行分析,舉一反四,解決了實際開發中的一些問題。下一章咱們將創造本身的瀑布流佈局,你也能有建立任意sliver佈局列表的能力。
歡迎加入Flutter Candies,一塊兒生產可愛的Flutter小糖果( QQ羣:181398081)
最最後放上Flutter Candies全家桶,真香。