前言
入坑Flutter一年了,接觸到Flutter也只是冰山一角,很多東西可能知道是怎么用的,但是不是很明白其中的原理,俗話說(shuō)唯有深入,方能淺出。本系列將對(duì)Sliver相關(guān)源碼一一進(jìn)行分析,希望能夠舉一反三,不再懼怕Sliver。
看完Flutter Sliver一生之?dāng)?你將不會(huì)害怕使用Sliver,Sliver將成為你的一生之愛(ài)。歡迎加入Flutter Candies <a target="_blank" ><img border="0" src="https://user-gold-cdn.xitu.io/2019/10/27/16e0ca3f1a736f0e?w=90&h=22&f=png&s=1827" alt="flutter-candies" title="flutter-candies"></a> QQ群: 181398081。
下面是全部滾動(dòng)的組件,以及他們的關(guān)系
| Widget | Build | Viewport |
|---|---|---|
| SingleChildScrollView | Scrollable | _SingleChildViewport |
| ScrollView | Scrollable | ShrinkWrappingViewport/Viewport |
Sliver系列繼承于ScrollView
| Widget | Extends |
|---|---|
| CustomScrollView | ScrollView |
| NestedScrollView | CustomScrollView |
| ListView/GridView | BoxScrollView => ScrollView |
簡(jiǎn)單講滾動(dòng)組件由Scrollable獲取用戶手勢(shì)反饋,將滾動(dòng)反饋和Slivers傳遞給Viewport計(jì)算出Sliver的位置。注意Sliver可以是單孩子(SliverPadding/SliverPersistentHeader/SliverToBoxAdapter等等)也可以是多孩子(SliverList/SliverGrid)。下面我們通過(guò)分析源碼,探究其中奧秘。
ScrollView
下面為build方法中的關(guān)鍵代碼,這里是我們上面說(shuō)的Scrollable,主要負(fù)責(zé)用戶手勢(shì)監(jiān)聽(tīng)反饋。
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
);
我們?cè)倏纯碽uildViewport方法
@protected
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
if (shrinkWrap) {
return ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
);
}
return Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
center: center,
anchor: anchor,
);
}
根據(jù)shrinkWrap的不同,分成了2種Viewport
Scrollable
用于監(jiān)聽(tīng)各種用戶手勢(shì)并實(shí)現(xiàn)滾動(dòng),下面為build方法中的關(guān)鍵代碼。
//InheritedWidget組件,為了共享position數(shù)據(jù)
Widget result = _ScrollableScope(
scrollable: this,
position: position,
// TODO(ianh): Having all these global keys is sad.
child: Listener(
onPointerSignal: _receivedPointerSignal,
child: RawGestureDetector(
key: _gestureDetectorKey,
gestures: _gestureRecognizers,
behavior: HitTestBehavior.opaque,
excludeFromSemantics: widget.excludeFromSemantics,
child: Semantics(
explicitChildNodes: !widget.excludeFromSemantics,
child: IgnorePointer(
key: _ignorePointerKey,
ignoring: _shouldIgnorePointer,
ignoringSemantics: false,
//通過(guò)Listener監(jiān)聽(tīng)手勢(shì),將滾動(dòng)position通過(guò)viewportBuilder回調(diào)。
child: widget.viewportBuilder(context, position),
),
),
),
),
);
//這里可以看到為什么安卓和ios上面對(duì)于滾動(dòng)越界(overscrolls)時(shí)候的操作不一樣
return _configuration.buildViewportChrome(context, result, widget.axisDirection);
安卓和fuchsia上面使用GlowingOverscrollIndicator來(lái)顯示滾動(dòng)不了之后的水波紋效果。
/// Wraps the given widget, which scrolls in the given [AxisDirection].
///
/// For example, on Android, this method wraps the given widget with a
/// [GlowingOverscrollIndicator] to provide visual feedback when the user
/// overscrolls.
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
// When modifying this function, consider modifying the implementation in
// _MaterialScrollBehavior as well.
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return child;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
color: _kDefaultGlowColor,
);
}
return null;
}
Viewport
通過(guò)只顯示(計(jì)算繪制)滾動(dòng)視圖中的一部分內(nèi)容來(lái)實(shí)現(xiàn)滾動(dòng)可視化設(shè)計(jì),大大降低內(nèi)存消耗。比如ListView可視區(qū)域?yàn)?66像素,但其列表元素的總高度遠(yuǎn)遠(yuǎn)超過(guò)666像素,但實(shí)際上我們只是關(guān)心這個(gè)666像素中的元素(當(dāng)然如果設(shè)置了CacheExtent,還要算上這個(gè)距離)
在Scrollview中將Scrollable滾動(dòng)反饋以及Slivers傳遞給了Viewport。Viewport 是一個(gè)MultiChildRenderObjectWidget,lei了lei了,這是一個(gè)自繪多孩子的組件。直接找到createRenderObject方法,看到返回一個(gè)RenderViewport
RenderViewport
重頭戲來(lái)了,我們看看構(gòu)造參數(shù)有哪些。
RenderViewport({
//主軸方向,默認(rèn)向下
AxisDirection axisDirection = AxisDirection.down,
//縱軸方向,跟主軸方向以及有關(guān)系
@required AxisDirection crossAxisDirection,
//Scrollable中回調(diào)的用戶反饋
@required ViewportOffset offset,
//當(dāng)scrollOffset = 0,第一個(gè)child在viewport的位置(0 <= anchor <= 1.0),0.0在leading,1.0在trailing,0.5在中間
double anchor = 0.0,
//sliver孩子們
List<RenderSliver> children,
//The first child in the [GrowthDirection.forward] growth direction.
//計(jì)算時(shí)候的基準(zhǔn),默認(rèn)為第一個(gè)娃,這個(gè)參數(shù)估計(jì)極少有人使用
RenderSliver center,
//緩存區(qū)域大小
double cacheExtent,
//決定cacheExtent是實(shí)際大小還是根據(jù)viewport的百分比
CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
})... {
addAll(children);
if (center == null && firstChild != null)
_center = firstChild;
}
可以看到構(gòu)造中把全部孩子都加進(jìn)入了,而且如果外部不傳遞center,center默認(rèn)為第一個(gè)孩子。
劃重點(diǎn)代碼分析
sizedByParent
在Viewport中這個(gè)值永遠(yuǎn)返回true,
@override
bool get sizedByParent => true;
來(lái)看看這個(gè)屬性的解釋。即如果這個(gè)值為true,那么組件的大小只跟它的parent告訴它的大小constraints有關(guān)系,與它的 child 都無(wú)關(guān).
就是說(shuō)RenderViewport的大小約束是由它的parent告訴它的,跟里面的Slivers沒(méi)有關(guān)系。說(shuō)到這個(gè)我們看一個(gè)新手經(jīng)常錯(cuò)誤的代碼。
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'測(cè)試',
),
ListView.builder(itemBuilder: (context,index){})
],
),
我們前面知道ListView最終是一個(gè)ScrollView,其中的Viewport在Column當(dāng)中是無(wú)法知道自己的有效大小的,該代碼的會(huì)導(dǎo)致Viewport的高度為無(wú)限大,將會(huì)報(bào)錯(cuò)(當(dāng)然你這里可以把shrinkWrap設(shè)置為true,但是這樣會(huì)導(dǎo)致ListView的全部元素都被計(jì)算,列表將失去滾動(dòng),這個(gè)我們后面會(huì)講)
繼續(xù)看代碼中看到,當(dāng)sizedByParent為true的時(shí)候調(diào)用performResize方法,指定Size只根據(jù)constraints。
if (sizedByParent) {
assert(() {
_debugDoingThisResize = true;
return true;
}());
try {
performResize();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_debugReportException('performResize', e, stack);
}
assert(() {
_debugDoingThisResize = false;
return true;
}());
}
performResize
看看RenderViewport的performResize中做了什么。有一大堆a(bǔ)ssert,就一句話,我不能無(wú)限大。最后將自己的size設(shè)置為constraints.biggest。
(size是自己的大小,constraints是parent給的限制)
@override
void performResize() {
assert(() {
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
switch (axis) {
case Axis.vertical:
if (!constraints.hasBoundedHeight) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Vertical viewport was given unbounded height.'),
ErrorDescription(
'Viewports expand in the scrolling direction to fill their container. '
'In this case, a vertical viewport was given an unlimited amount of '
'vertical space in which to expand. This situation typically happens '
'when a scrollable widget is nested inside another scrollable widget.'
),
ErrorHint(
'If this widget is always nested in a scrollable widget there '
'is no need to use a viewport because there will always be enough '
'vertical space for the children. In this case, consider using a '
'Column instead. Otherwise, consider using the "shrinkWrap" property '
'(or a ShrinkWrappingViewport) to size the height of the viewport '
'to the sum of the heights of its children.'
)
]);
}
if (!constraints.hasBoundedWidth) {
throw FlutterError(
'Vertical viewport was given unbounded width.\n'
'Viewports expand in the cross axis to fill their container and '
'constrain their children to match their extent in the cross axis. '
'In this case, a vertical viewport was given an unlimited amount of '
'horizontal space in which to expand.'
);
}
break;
case Axis.horizontal:
if (!constraints.hasBoundedWidth) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Horizontal viewport was given unbounded width.'),
ErrorDescription(
'Viewports expand in the scrolling direction to fill their container.'
'In this case, a horizontal viewport was given an unlimited amount of '
'horizontal space in which to expand. This situation typically happens '
'when a scrollable widget is nested inside another scrollable widget.'
),
ErrorHint(
'If this widget is always nested in a scrollable widget there '
'is no need to use a viewport because there will always be enough '
'horizontal space for the children. In this case, consider using a '
'Row instead. Otherwise, consider using the "shrinkWrap" property '
'(or a ShrinkWrappingViewport) to size the width of the viewport '
'to the sum of the widths of its children.'
)
]);
}
if (!constraints.hasBoundedHeight) {
throw FlutterError(
'Horizontal viewport was given unbounded height.\n'
'Viewports expand in the cross axis to fill their container and '
'constrain their children to match their extent in the cross axis. '
'In this case, a horizontal viewport was given an unlimited amount of '
'vertical space in which to expand.'
);
}
break;
}
}
return true;
}());
size = constraints.biggest;
// We ignore the return value of applyViewportDimension below because we are
// going to go through performLayout next regardless.
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
performLayout
負(fù)責(zé)布局RenderViewport的Children
//從size中得到主軸和縱軸的大小
double mainAxisExtent;
double crossAxisExtent;
switch (axis) {
case Axis.vertical:
mainAxisExtent = size.height;
crossAxisExtent = size.width;
break;
case Axis.horizontal:
mainAxisExtent = size.width;
crossAxisExtent = size.height;
break;
}
//如果單Sliver孩子的viewport高度為100,anchor為0.5,centerOffsetAdjustment設(shè)置為50.0的話,當(dāng)scroll offset is 0.0的時(shí)候,center會(huì)剛好在viewport中間。
final double centerOffsetAdjustment = center.centerOffsetAdjustment;
double correction;
int count = 0;
do {
assert(offset.pixels != null);
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
///如果不為0.0的話,是因?yàn)閏hild中有需要修正(這個(gè)我們將在后面系列中講到,這里我們就簡(jiǎn)單認(rèn)為在layout child過(guò)程中出現(xiàn)了問(wèn)題),我們需要改變scroll offset之后重新layout chilren。
if (correction != 0.0) {
offset.correctBy(correction);
} else {
///告訴Scrollable 最小滾動(dòng)距離和最大滾動(dòng)距離
if (offset.applyContentDimensions(
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
))
break;
}
count += 1;
} while (count < _maxLayoutCycles);
如果超過(guò)最大次數(shù),children還是layout還是有問(wèn)題的話,將警告提示。
下面我們看看_attemptLayout方法中做了什么。
double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
assert(!mainAxisExtent.isNaN);
assert(mainAxisExtent >= 0.0);
assert(crossAxisExtent.isFinite);
assert(crossAxisExtent >= 0.0);
assert(correctedOffset.isFinite);
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
//centerOffset的數(shù)值將使用anchor和offset.pixels + centerOffsetAdjustment進(jìn)行修正。前面有講
final double centerOffset = mainAxisExtent * anchor - correctedOffset;
//反向RemainingPaintExtent,就是center之前還有多少距離可以拿來(lái)繪制
final double reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent);
//正向RemainingPaintExtent,就是center之后還有多少距離可以拿來(lái)繪制
final double forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent);
switch (cacheExtentStyle) {
case CacheExtentStyle.pixel:
_calculatedCacheExtent = cacheExtent;
break;
case CacheExtentStyle.viewport:
_calculatedCacheExtent = mainAxisExtent * cacheExtent;
break;
}
///總的計(jì)算區(qū)域包含前后2個(gè)cacheExtent
final double fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent;
///加上cacheExtent的center位置,跟前面的比就是多了cache
final double centerCacheOffset = centerOffset + _calculatedCacheExtent;
//反向RemainingPaintExtent,就是center之前還有多少距離可以拿來(lái)繪制,跟前面的比就是多了cache
final double reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent);
//正向RemainingPaintExtent,就是center之后還有多少距離可以拿來(lái)繪制,跟前面的比就是多了cache
final double forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent);
final RenderSliver leadingNegativeChild = childBefore(center);
///如果在center之前還有child,將向前l(fā)ayout child,計(jì)算前面布局前面的child
if (leadingNegativeChild != null) {
// negative scroll offsets
final double result = layoutChildSequence(
child: leadingNegativeChild,
scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
overlap: 0.0,
layoutOffset: forwardDirectionRemainingPaintExtent,
remainingPaintExtent: reverseDirectionRemainingPaintExtent,
mainAxisExtent: mainAxisExtent,
crossAxisExtent: crossAxisExtent,
growthDirection: GrowthDirection.reverse,
advance: childBefore,
remainingCacheExtent: reverseDirectionRemainingCacheExtent,
cacheOrigin: (mainAxisExtent - centerOffset).clamp(-_calculatedCacheExtent, 0.0),
);
if (result != 0.0)
return -result;
}
///布局center后面的child
// positive scroll offsets
return layoutChildSequence(
child: center,
scrollOffset: math.max(0.0, -centerOffset),
overlap: leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
layoutOffset: centerOffset >= mainAxisExtent ? centerOffset: reverseDirectionRemainingPaintExtent,
remainingPaintExtent: forwardDirectionRemainingPaintExtent,
mainAxisExtent: mainAxisExtent,
crossAxisExtent: crossAxisExtent,
growthDirection: GrowthDirection.forward,
advance: childAfter,
remainingCacheExtent: forwardDirectionRemainingCacheExtent,
cacheOrigin: centerOffset.clamp(-_calculatedCacheExtent, 0.0),
);
}
注意scrollOffset ,在向前和向后layout的時(shí)候不一樣,
一個(gè)是 math.max(mainAxisExtent, centerOffset) - mainAxisExtent
一個(gè)是 math.max(0.0, -centerOffset)
我們有說(shuō)過(guò)center其實(shí)是scrolloffset為0的基準(zhǔn),viewport里面如果有多個(gè)slivers,我們可以指定其中一個(gè)為center(默認(rèn)第一個(gè)為center),那么想前滾centerOffset會(huì)變大,想后滾centerOffset會(huì)變成負(fù)數(shù)。感覺(jué)還是有點(diǎn)抽象,下面給一個(gè)栗子,我給第2個(gè)sliver增加了key,并且把CustomScrollView的center賦值為這個(gè)key。小聲逼逼,Center這個(gè)參數(shù)我估計(jì)百分之99的人沒(méi)有用過(guò),用過(guò)的請(qǐng)留言,我看看有多少人知道這個(gè)。
CustomScrollView(
center: key,
slivers: <Widget>[
SliverList(),
SliverGrid(key:key),
運(yùn)行起來(lái)初始centerOffset為0的時(shí)候SliverGrid在初始位置。
向前滾動(dòng),可以看到我們得到了逆向的SliverList,從我們的參數(shù)中也可以驗(yàn)證到。而offset.pixels(ScollView的滾動(dòng)位置)當(dāng)然也為0.(而不是你們想的SliverList的高度)
再看下layoutChildSequence方法,注意到advance方法,向前其實(shí)調(diào)用的是childBefore,向后是調(diào)用的childAfter
double layoutChildSequence({
@required RenderSliver child,
@required double scrollOffset,
@required double overlap,
@required double layoutOffset,
@required double remainingPaintExtent,
@required double mainAxisExtent,
@required double crossAxisExtent,
@required GrowthDirection growthDirection,
@required RenderSliver advance(RenderSliver child),
@required double remainingCacheExtent,
@required double cacheOrigin,
}) {
assert(scrollOffset.isFinite);
assert(scrollOffset >= 0.0);
final double initialLayoutOffset = layoutOffset;
final ScrollDirection adjustedUserScrollDirection =
applyGrowthDirectionToScrollDirection(offset.userScrollDirection, growthDirection);
assert(adjustedUserScrollDirection != null);
double maxPaintOffset = layoutOffset + overlap;
double precedingScrollExtent = 0.0;
while (child != null) {
final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;
// If the scrollOffset is too small we adjust the paddedOrigin because it
// doesn't make sense to ask a sliver for content before its scroll
// offset.
final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);
final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;
assert(sliverScrollOffset >= correctedCacheOrigin.abs());
assert(correctedCacheOrigin <= 0.0);
assert(sliverScrollOffset >= 0.0);
assert(cacheExtentCorrection <= 0.0);
//輸入
child.layout(SliverConstraints(
axisDirection: axisDirection,
growthDirection: growthDirection,
userScrollDirection: adjustedUserScrollDirection,
scrollOffset: sliverScrollOffset,
precedingScrollExtent: precedingScrollExtent,
overlap: maxPaintOffset - layoutOffset,
remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
crossAxisExtent: crossAxisExtent,
crossAxisDirection: crossAxisDirection,
viewportMainAxisExtent: mainAxisExtent,
remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
cacheOrigin: correctedCacheOrigin,
), parentUsesSize: true);
//輸出
final SliverGeometry childLayoutGeometry = child.geometry;
assert(childLayoutGeometry.debugAssertIsValid());
// If there is a correction to apply, we'll have to start over.
if (childLayoutGeometry.scrollOffsetCorrection != null)
return childLayoutGeometry.scrollOffsetCorrection;
// We use the child's paint origin in our coordinate system as the
// layoutOffset we store in the child's parent data.
final double effectiveLayoutOffset = layoutOffset + childLayoutGeometry.paintOrigin;
// `effectiveLayoutOffset` becomes meaningless once we moved past the trailing edge
// because `childLayoutGeometry.layoutExtent` is zero. Using the still increasing
// 'scrollOffset` to roughly position these invisible slivers in the right order.
if (childLayoutGeometry.visible || scrollOffset > 0) {
updateChildLayoutOffset(child, effectiveLayoutOffset, growthDirection);
} else {
updateChildLayoutOffset(child, -scrollOffset + initialLayoutOffset, growthDirection);
}
//更新最大繪制位置
maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
scrollOffset -= childLayoutGeometry.scrollExtent;
//前一個(gè)child的滾動(dòng)距離
precedingScrollExtent += childLayoutGeometry.scrollExtent;
layoutOffset += childLayoutGeometry.layoutExtent;
if (childLayoutGeometry.cacheExtent != 0.0) {
remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
}
// 更新_maxScrollExtent和_minScrollExtent
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/rendering/viewport.dart#L1449
updateOutOfBandData(growthDirection, childLayoutGeometry);
// move on to the next child
// layout下一個(gè)child
child = advance(child);
}
// we made it without a correction, whee!
//完美,全部的children都沒(méi)有錯(cuò)誤
return 0.0;
}
SliverConstraints為layout child的輸入,SliverGeometry為layout child之后的輸出,layout之后viewport將更新_maxScrollExtent和_minScrollExtent,然后layout下一個(gè)sliver。至于child.layout方法里面內(nèi)容,我們將會(huì)在下一個(gè)章當(dāng)中講到。
RenderShrinkWrappingViewport
當(dāng)我們把shrinkWrap設(shè)置為true的時(shí)候,最終的Viewport使用的是RenderShrinkWrappingViewport。那么我們看看其中的區(qū)別是什么。
先看看官方對(duì)shrinkWrap參數(shù)的解釋。設(shè)置shrinkWrap為true,viewport的大小將不是由它的父親而決定,而是由它自己決定。我們經(jīng)常碰到由人使用ListView嵌套ListView的情況, 外面的ListView在layout child的時(shí)候需要知道里面ListView的大小,而我們前面知道ListView中的Viewport的大小是由它parent告訴它的。
parent:hi, child,你有多大,我給你一個(gè)無(wú)限縱軸大小的限制。
child: hi, parent,我也不知道啊,你不告訴我,我的viewport有多大。那么我只能將我的全部child都layout出來(lái)才知道我總的大小了。那我得換一個(gè)viewport了,RenderShrinkWrappingViewport才能知道計(jì)算出我的總高度。
由于ListView的parent無(wú)法告訴它的child ListView的可丈量大小,所以我們必須設(shè)置shrinkWrap為true,內(nèi)部使用RenderShrinkWrappingViewport計(jì)算。
由于RenderShrinkWrappingViewport的大小不再只由parent決定,所以不再調(diào)用performResize方法。那么我們來(lái)關(guān)注下performLayout方法。
performLayout
@override
void performLayout() {
if (firstChild == null) {
switch (axis) {
case Axis.vertical:
//如果是豎直,你起碼要告訴我水平最大限制吧?
assert(constraints.hasBoundedWidth);
size = Size(constraints.maxWidth, constraints.minHeight);
break;
//如果是水平,你起碼要告訴我垂直最大限制吧?
case Axis.horizontal:
assert(constraints.hasBoundedHeight);
size = Size(constraints.minWidth, constraints.maxHeight);
break;
}
offset.applyViewportDimension(0.0);
_maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0;
_hasVisualOverflow = false;
offset.applyContentDimensions(0.0, 0.0);
return;
}
double mainAxisExtent;
double crossAxisExtent;
switch (axis) {
case Axis.vertical:
//如果是豎直,你起碼要告訴我水平最大限制吧?說(shuō)到這個(gè)我想起來(lái)了Flutter中為啥沒(méi)有支持水平和垂直都能滾動(dòng)的容器了。
assert(constraints.hasBoundedWidth);
mainAxisExtent = constraints.maxHeight;
crossAxisExtent = constraints.maxWidth;
break;
case Axis.horizontal:
assert(constraints.hasBoundedHeight);
//如果是水平,你起碼要告訴我垂直最大限制吧?
mainAxisExtent = constraints.maxWidth;
crossAxisExtent = constraints.maxHeight;
break;
}
double correction;
double effectiveExtent;
do {
assert(offset.pixels != null);
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels);
if (correction != 0.0) {
offset.correctBy(correction);
} else {
switch (axis) {
case Axis.vertical:
effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent);
break;
case Axis.horizontal:
effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent);
break;
}
final bool didAcceptViewportDimension = offset.applyViewportDimension(effectiveExtent);
final bool didAcceptContentDimension = offset.applyContentDimensions(0.0, math.max(0.0, _maxScrollExtent - effectiveExtent));
if (didAcceptViewportDimension && didAcceptContentDimension)
break;
}
} while (true);
switch (axis) {
case Axis.vertical:
size = constraints.constrainDimensions(crossAxisExtent, effectiveExtent);
break;
case Axis.horizontal:
size = constraints.constrainDimensions(effectiveExtent, crossAxisExtent);
break;
}
}
_maxScrollExtent和
_shrinkWrapExtent都是關(guān)鍵先生。當(dāng)mainAxisExtent不為double.Infinity(無(wú)限大)的時(shí)候,其實(shí)效果跟Viewport里面計(jì)算(除掉Center相關(guān))是一樣; 當(dāng)mainAxisExtent為double.Infinity(無(wú)限大),我們將會(huì)將全部的child都layout出來(lái)獲得總的大小
@override
void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
assert(growthDirection == GrowthDirection.forward);
_maxScrollExtent += childLayoutGeometry.scrollExtent;
if (childLayoutGeometry.hasVisualOverflow)
_hasVisualOverflow = true;
_shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
}
這里也就是為啥我們之前說(shuō)Column里面或者ListView放ListView(子),ListView(子)會(huì)全部元素都build,并且失去滾動(dòng)的原因。
劇透
這一章看起來(lái)有些枯燥,都是源碼分析。下一章(Flutter Sliver一生之?dāng)?(ExtendedList)),我們將順著ListView/GridView=> SliverList/SliverGrid => RenderSliverList/RenderSliverGrid的感情線,了解最終Sliver是怎么將children繪制出來(lái)的。下一章將不只是枯燥的源碼分析,我們將舉一反N,告訴你如何處理圖片列表內(nèi)存爆炸閃退,將告訴你列表元素特殊的layout方式等等。
結(jié)語(yǔ)
ExtendedList WaterfallFlow 和 LoadingMoreList 都是可以食用的狀態(tài)。等不及的小伙伴可以提前食用,特別是圖片列表內(nèi)存過(guò)大而導(dǎo)致閃退的小伙伴可以先看demo,先解決掉一直折磨大家的問(wèn)題
歡迎加入Flutter Candies,一起生產(chǎn)可愛(ài)的Flutter小糖果( <a target="_blank" ><img border="0" src="https://user-gold-cdn.xitu.io/2019/10/27/16e0ca3f1a736f0e?w=90&h=22&f=png&s=1827" alt="flutter-candies" title="flutter-candies"></a>QQ群:181398081)
最最后放上Flutter Candies全家桶,真香。