Flutter框架中有不少滾動的Widget,ListView、GridView等,這些Widget都是使用Scrollable配合Viewport來完成滾動的。咱們來分析一下這個滾動效果是怎樣實現的。ios
Scrollable繼承自StatefulWidget,咱們看一下他的State的build方法來看一下他的構成緩存
@override
Widget build(BuildContext context) {
assert(position != null);
Widget result = _ScrollableScope(
scrollable: this,
position: position,
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,
child: widget.viewportBuilder(context, position),
),
),
),
);
...省略不重要的
return _configuration.buildViewportChrome(context, result, widget.axisDirection);
}
複製代碼
能夠看到最主要的兩點就是:RawGestureDetector來監聽用戶手勢,viewportBuilder來建立Viewportapp
Scrollable中有一個重要的字段就是ScrollPosition(繼承自ViewportOffset,ViewportOffset又繼承自ChangeNotifier),ViewportOffset是viewportBuilder中的一個重要參數,用來描述Viewport的偏移量。ScrollPosition是在_updatePosition方法中進行更新和建立的。框架
void _updatePosition() {
_configuration = ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context);
if (widget.physics != null)
_physics = widget.physics.applyTo(_physics);
final ScrollController controller = widget.controller;
final ScrollPosition oldPosition = position;
if (oldPosition != null) {
controller?.detach(oldPosition);
scheduleMicrotask(oldPosition.dispose);
}
//更新_position
_position = controller?.createScrollPosition(_physics, this, oldPosition)
?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
assert(position != null);
controller?.attach(position);
}
複製代碼
能夠看到ScrollPosition的實例是ScrollPositionWithSingleContext,並且_updatePosition是在didChangeDependencies以及didUpdateWidget方法中調用的(在Element更新的狀況下都會去更新position)。ide
咱們繼續看Scrollable中的手勢監聽_handleDragDown、_handleDragStart、_handleDragUpdate、_handleDragEnd、_handleDragCancel這五個方法來處理用戶的手勢。佈局
void _handleDragDown(DragDownDetails details) {
assert(_drag == null);
assert(_hold == null);
_hold = position.hold(_disposeHold);
}
@override
ScrollHoldController hold(VoidCallback holdCancelCallback) {
final double previousVelocity = activity.velocity;
final HoldScrollActivity holdActivity = HoldScrollActivity(
delegate: this,
onHoldCanceled: holdCancelCallback,
);
beginActivity(holdActivity);//開始HoldScrollActivity活動
_heldPreviousVelocity = previousVelocity;
return holdActivity;
}
複製代碼
能夠看到_handleDragDown中就是調用ScrollPosition的hold方法返回一個holdActivity。咱們繼續看一下_handleDragStart性能
void _handleDragStart(DragStartDetails details) {
assert(_drag == null);
_drag = position.drag(details, _disposeDrag);
assert(_drag != null);
assert(_hold == null);
}
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
final ScrollDragController drag = ScrollDragController(
delegate: this,
details: details,
onDragCanceled: dragCancelCallback,
carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
);
beginActivity(DragScrollActivity(this, drag));//開始DragScrollActivity活動
assert(_currentDrag == null);
_currentDrag = drag;
return drag;//返回ScrollDragController
}
複製代碼
_handleDragStart中調用ScrollPosition的drag方法可是返回的ScrollDragController對象,並無返回DragScrollActivity。咱們繼續看一下_handleDragUpdate、_handleDragEnd、_handleDragCancel方法測試
void _handleDragUpdate(DragUpdateDetails details) {
assert(_hold == null || _drag == null);
_drag?.update(details);
}
void _handleDragEnd(DragEndDetails details) {
assert(_hold == null || _drag == null);
_drag?.end(details);
assert(_drag == null);
}
void _handleDragCancel() {
assert(_hold == null || _drag == null);
_hold?.cancel();
_drag?.cancel();
assert(_hold == null);
assert(_drag == null);
}
複製代碼
_handleDragUpdate、_handleDragEnd、_handleDragCancel基本就是調用_hold,_drag的對應的方法。咱們先看一下ScrollPositionWithSingleContext中的beginActivity方法ui
@override
void beginActivity(ScrollActivity newActivity) {
_heldPreviousVelocity = 0.0;
if (newActivity == null)
return;
assert(newActivity.delegate == this);
super.beginActivity(newActivity);
_currentDrag?.dispose();
_currentDrag = null;
if (!activity.isScrolling)
updateUserScrollDirection(ScrollDirection.idle);
}
///ScrollPosition的beginActivity方法
void beginActivity(ScrollActivity newActivity) {
if (newActivity == null)
return;
bool wasScrolling, oldIgnorePointer;
if (_activity != null) {
oldIgnorePointer = _activity.shouldIgnorePointer;
wasScrolling = _activity.isScrolling;
if (wasScrolling && !newActivity.isScrolling)
didEndScroll();
_activity.dispose();
} else {
oldIgnorePointer = false;
wasScrolling = false;
}
_activity = newActivity;
if (oldIgnorePointer != activity.shouldIgnorePointer)
context.setIgnorePointer(activity.shouldIgnorePointer);
isScrollingNotifier.value = activity.isScrolling;
if (!wasScrolling && _activity.isScrolling)
didStartScroll();
}
複製代碼
ScrollPosition的beginActivity總結下來就是發送相關的ScrollNotification(咱們用NotificationListener能夠監聽)以及dispose上一個activity,ScrollPositionWithSingleContext的beginActivity方法後續會調用updateUserScrollDirection方法來更新以及發送UserScrollDirection。this
看到這裏咱們能夠發現Scrollable的第一個做用就是發送ScrollNotification。咱們繼續看一下update時的狀況,_handleDragUpdate就是調用Drag的update方法,咱們直接看update方法,它的具體實現是ScrollDragController
@override
void update(DragUpdateDetails details) {
assert(details.primaryDelta != null);
_lastDetails = details;
double offset = details.primaryDelta;
if (offset != 0.0) {
_lastNonStationaryTimestamp = details.sourceTimeStamp;
}
_maybeLoseMomentum(offset, details.sourceTimeStamp);
offset = _adjustForScrollStartThreshold(offset, details.sourceTimeStamp);//根據ios的彈性滑動調整offset
if (offset == 0.0) {
return;
}
if (_reversed)
offset = -offset;
delegate.applyUserOffset(offset);//調用ScrollPositionWithSingleContext的applyUserOffset方法
}
複製代碼
主要看最後applyUserOffset方法
@override
void applyUserOffset(double delta) {
updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);//發送UserScrollNotification
setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));//setPixels直接調用了super.setPixels
}
double setPixels(double newPixels) {
assert(_pixels != null);
assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index);
if (newPixels != pixels) {
final double overscroll = applyBoundaryConditions(newPixels);//計算出overscroll
assert(() {
final double delta = newPixels - pixels;
if (overscroll.abs() > delta.abs()) {
throw FlutterError();
}
return true;
}());
final double oldPixels = _pixels;
_pixels = newPixels - overscroll;//計算出滾動距離
if (_pixels != oldPixels) {
notifyListeners();//通知Listeners,由於ScrollPosition繼承自ChangeNotifier,能夠設置Listeners,這裏也是直接調用了ChangeNotifier中的notifyListeners方法
didUpdateScrollPositionBy(_pixels - oldPixels);//調用activity發送ScrollUpdateNotification
}
if (overscroll != 0.0) {
didOverscrollBy(overscroll);//調用activity發送OverscrollNotification
return overscroll;
}
}
return 0.0;
}
複製代碼
applyUserOffset方法中調用了一個很是重要的notifyListeners方法,那麼這些Listeners是在哪設置的呢?在RenderViewport中找到了它的設置地方
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(markNeedsLayout);//直接標記從新layout
}
@override
void detach() {
_offset.removeListener(markNeedsLayout);
super.detach();
}
複製代碼
能夠看到在RenderObject attach的時候添加監聽,在detach的時候移除監聽,至於監聽中的實現,在_RenderSingleChildViewport中有不一樣的實現。
到此咱們能夠總結出Scrollable的主要做用了
咱們先看只包含一個Child的Viewport
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(_hasScrolled);
}
@override
void detach() {
_offset.removeListener(_hasScrolled);
super.detach();
}
void _hasScrolled() {
markNeedsPaint();
markNeedsSemanticsUpdate();
}
複製代碼
在_RenderSingleChildViewport中當發生滾動的時候時只須要重繪的,咱們先看一下他怎樣進行佈局的
@override
void performLayout() {
if (child == null) {
size = constraints.smallest;
} else {
child.layout(_getInnerConstraints(constraints), parentUsesSize: true);//計算child的約束去佈局child
size = constraints.constrain(child.size);//本身的size最大不能超過自身的Box約束
}
offset.applyViewportDimension(_viewportExtent);
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
}
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
switch (axis) {
case Axis.horizontal:
return constraints.heightConstraints();//橫向滾動,就返回高度按parent傳進來的約束,寬度約束就是0到無窮大
case Axis.vertical:
return constraints.widthConstraints();//縱向滾動,就返回寬度按parent傳進來的約束,高度約束就是0到無窮大
}
return null;
}
複製代碼
看一下offset.applyViewportDimension方法,offset是傳入的ViewportOffset,_viewportExtent(視窗範圍),看一下其get方法
double get _viewportExtent {
assert(hasSize);
switch (axis) {
case Axis.horizontal:
return size.width;//橫向滾動,就返回自身size的寬度
case Axis.vertical:
return size.height;//縱向滾動,就返回自身size的高度
}
return null;
}
@override
bool applyViewportDimension(double viewportDimension) {
if (_viewportDimension != viewportDimension) {
_viewportDimension = viewportDimension;//簡單的賦值
_didChangeViewportDimensionOrReceiveCorrection = true;
}
return true;
}
複製代碼
offset.applyViewportDimension就是簡單的計算viewportExtent的值並賦值給ScrollPosition。咱們在看一下offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent)方法
double get _minScrollExtent {
assert(hasSize);
return 0.0;
}
double get _maxScrollExtent {
assert(hasSize);
if (child == null)
return 0.0;
switch (axis) {
case Axis.horizontal:
return math.max(0.0, child.size.width - size.width);
case Axis.vertical:
return math.max(0.0, child.size.height - size.height);
}
return null;
}
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
_didChangeViewportDimensionOrReceiveCorrection) {
_minScrollExtent = minScrollExtent;//簡單的賦值
_maxScrollExtent = maxScrollExtent;//簡單的賦值
_haveDimensions = true;
applyNewDimensions();//通知活動viewport的尺寸或者內容發生了改變
_didChangeViewportDimensionOrReceiveCorrection = false;
}
return true;
}
複製代碼
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent)方法也基本上就是計算minScrollExtent、maxScrollExtent而後進行賦值。
咱們在看paint方法
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Offset paintOffset = _paintOffset;//計算出繪製偏移
void paintContents(PaintingContext context, Offset offset) {
context.paintChild(child, offset + paintOffset);//加上繪製偏移去繪製child
}
if (_shouldClipAtPaintOffset(paintOffset)) {//看是否須要裁剪
context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintContents);
} else {
paintContents(context, offset);
}
}
}
Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);
Offset _paintOffsetForPosition(double position) {
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up://往上滾動,把內容網上偏移繪製
return Offset(0.0, position - child.size.height + size.height);
case AxisDirection.down:
return Offset(0.0, -position);//往下滾動,把內容網上偏移繪製
case AxisDirection.left:
return Offset(position - child.size.width + size.width, 0.0);
case AxisDirection.right:
return Offset(-position, 0.0);
}
return null;
}
bool _shouldClipAtPaintOffset(Offset paintOffset) {
assert(child != null);
//這句話的意思能夠翻譯成這樣:繪製內容的左上座標以及右下座標是否在Viewport的size裏面,不然就須要裁剪
return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight);
}
複製代碼
能夠看到單個child的viewport仍是使用的盒約束去佈局child,並且它的滾動效果實現就是經過繪製偏移來實現的。
咱們上面知道RenderViewport在offset改變時會從新去佈局繪製,由於在RenderViewport重寫了sizedByParent,那麼它自身的size是在performResize中肯定的,咱們先看performResize
@override
void performResize() {
size = constraints.biggest;//肯定本身的size爲約束的最大範圍
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);//賦值ViewportDimension
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
複製代碼
而後咱們繼續看performLayout
@override
void performLayout() {
if (center == null) {
assert(firstChild == null);
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
offset.applyContentDimensions(0.0, 0.0);
return;
}
assert(center.parent == this);
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;
}
final double centerOffsetAdjustment = center.centerOffsetAdjustment;
double correction;
int count = 0;
do {
assert(offset.pixels != null);
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
if (correction != 0.0) {
offset.correctBy(correction);
} else {
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);
}
複製代碼
performLayout裏面存在一個循環,只要哪一個元素佈局的過程當中須要調整滾動的偏移量,就會更新滾動偏移量以後再從新佈局,可是從新佈局的次數不能超過_kMaxLayoutCycles也就是10次,這裏也是明顯從性能考慮;看一下_attemptLayout方法
double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
//第一個sliver佈局開始點的偏移
final double centerOffset = mainAxisExtent * anchor - correctedOffset;
//反向餘留的繪製範圍
final double reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent);
//正向餘留的繪製範圍
final double forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent);
//總共的緩存範圍
final double fullCacheExtent = mainAxisExtent + 2 * cacheExtent;
final double centerCacheOffset = centerOffset + cacheExtent;
//反向餘留的緩存範圍
final double reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent);
//正向餘留的緩存範圍
final double forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent);
final RenderSliver leadingNegativeChild = childBefore(center);
if (leadingNegativeChild != null) {
//反向滾動
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(-cacheExtent, 0.0),
);
if (result != 0.0)
return -result;
}
//正向滾動
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(-cacheExtent, 0.0),
);
}
複製代碼
這裏面能夠看到就是一些變量的賦值,而後根據正向反向來進行佈局,這裏咱們先要說明一下這幾個變量的意思
咱們繼續看layoutChildSequence方法
@protected
double layoutChildSequence({
@required RenderSliver child,//佈局的起始child,類型必須是RenderSliver
@required double scrollOffset,//centerSliver的偏移量
@required double overlap,
@required double layoutOffset,//佈局的偏移量
@required double remainingPaintExtent,//剩餘須要繪製的範圍
@required double mainAxisExtent,//viewport的主軸範圍
@required double crossAxisExtent,//viewport的縱軸範圍
@required GrowthDirection growthDirection,//增加方向
@required RenderSliver advance(RenderSliver child),
@required double remainingCacheExtent,//剩餘須要緩存的範圍
@required double cacheOrigin,//緩存的起點
}) {
//將傳進來的layoutOffset記錄爲初始佈局偏移
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) {
//計算sliver的滾動偏移,scrollOffset <= 0.0表示當前sliver的偏移量還沒越過viewport頂部,尚未輪到該sliver滾動,因此sliver的滾動偏移爲0
final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;
final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);
final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;
//建立SliverConstraints去佈局child
child.layout(SliverConstraints(
axisDirection: axisDirection,//主軸方向
growthDirection: growthDirection,//sliver的排列方向
userScrollDirection: adjustedUserScrollDirection,//用戶滾動方向
scrollOffset: sliverScrollOffset,//sliver的滾動偏移量
precedingScrollExtent: precedingScrollExtent,//被前面sliver消費的滾動距離
overlap: maxPaintOffset - layoutOffset,
remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),//sliver仍然須要繪製的範圍
crossAxisExtent: crossAxisExtent,//縱軸的範圍
crossAxisDirection: crossAxisDirection,
viewportMainAxisExtent: mainAxisExtent,//viewport主軸的範圍
remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),//sliver仍然須要緩存的範圍
cacheOrigin: correctedCacheOrigin,
), parentUsesSize: true);
final SliverGeometry childLayoutGeometry = child.geometry;
assert(childLayoutGeometry.debugAssertIsValid());
//scrollOffsetCorrection若是不爲空,就要從新開始佈局
if (childLayoutGeometry.scrollOffsetCorrection != null)
return childLayoutGeometry.scrollOffsetCorrection;
//計算sliver的layout偏移
final double effectiveLayoutOffset = layoutOffset + childLayoutGeometry.paintOrigin;
//記錄sliver的layout偏移
if (childLayoutGeometry.visible || scrollOffset > 0) {
updateChildLayoutOffset(child, effectiveLayoutOffset, growthDirection);
} else {
updateChildLayoutOffset(child, -scrollOffset + initialLayoutOffset, growthDirection);
}
//更新最大繪製偏移
maxPaintOffset = math.max(effectiveLayoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
//計算下一個sliver的scrollOffset(center的sliver的scrollOffset是centerOffset)
scrollOffset -= childLayoutGeometry.scrollExtent;
//統計前面的sliver總共消耗的滾動範圍
precedingScrollExtent += childLayoutGeometry.scrollExtent;
//計算下一個sliver的佈局偏移
layoutOffset += childLayoutGeometry.layoutExtent;
if (childLayoutGeometry.cacheExtent != 0.0) {
//計算餘下的緩存範圍,remainingCacheExtent須要減去當前sliver所用掉的cacheExtent
remainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;
//計算下一個sliver的緩存起始
cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);
}
updateOutOfBandData(growthDirection, childLayoutGeometry);
佈局下一個sliver
child = advance(child);
}
//正確完成佈局直接返回0
return 0.0;
}
複製代碼
從layout的過程咱們能夠看到,viewport佈局每個child的時候是計算一個sliver約束去佈局,讓後更新每一個sliver的layoutOffset。那咱們再看一下viewport的繪製過程
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
if (hasVisualOverflow) {
//viewport有內容溢出就使用clip繪製
context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintContents);
} else {
_paintContents(context, offset);
}
}
void _paintContents(PaintingContext context, Offset offset) {
for (RenderSliver child in childrenInPaintOrder) {
//sliver是否顯示,不然不繪製
if (child.geometry.visible)
//將layoutOffset運用的繪製偏移中,來定位每個sliver
context.paintChild(child, offset + paintOffsetOf(child));
}
}
@override
Offset paintOffsetOf(RenderSliver child) {
final SliverPhysicalParentData childParentData = child.parentData;
return childParentData.paintOffset;
}
複製代碼
從viewport的size、layout、paint過程咱們能夠知道,viewport只肯定sliver的layoutExtent、paintExtent(大小)以及layoutOffset(位置),而後對每一個sliver進行繪製。 咱們有一張圖大體能夠表示viewport的佈局繪製過程,只肯定每一個sliver的大小以及位置,不顯示的sliver不進行繪製;至於sliver內的內容滾動了多少,該怎樣去佈局繪製,viewport只傳入了sliver約束,讓sliver自行去處理。
這兩個是相對出現了,跟BoxConstraints與Size的關係同樣,一個做爲輸入(SliverConstraints),一個做爲輸出(SliverGeometry)
SliverConstraints({
@required this.axisDirection,//scrollOffset、remainingPaintExtent增加的方向
@required this.growthDirection,//sliver排列的方向
@required this.userScrollDirection,//用戶滾動的方向,viewport的scrollOffset爲正直是爲forward,負值爲reverse,沒有滾動則爲idle
@required this.scrollOffset,//在sliver座標系中的滾動偏移量
@required this.precedingScrollExtent,//前面sliver已經消耗的滾動距離,等於前面sliver的scrollExtent的累加結果
@required this.overlap,//指前一個Sliver組件的layoutExtent(佈局區域)和paintExtent(繪製區域)重疊了的區域大小
@required this.remainingPaintExtent,//viewport仍剩餘的繪製範圍
@required this.crossAxisExtent,//viewport滾動軸縱向的範圍
@required this.crossAxisDirection,//viewport滾動軸縱向的方向
@required this.viewportMainAxisExtent,//viewport滾動軸的範圍
@required this.remainingCacheExtent,//viewport仍剩餘的緩存範圍
@required this.cacheOrigin,//緩存起始
})
SliverGeometry({
this.scrollExtent = 0.0,//sliver能夠滾動內容的總範圍
this.paintExtent = 0.0,//sliver容許繪製的範圍
this.paintOrigin = 0.0,//sliver的繪製起始
double layoutExtent,//sliver的layout範圍
this.maxPaintExtent = 0.0,//最大的繪製範圍,
this.maxScrollObstructionExtent = 0.0,//當sliver被固定住,sliver能夠減小內容滾動的區域的最大範圍
double hitTestExtent,//命中測試的範圍
bool visible,//是否可見,sliver是否應該被繪製
this.hasVisualOverflow = false,//sliver是否有視覺溢出
this.scrollOffsetCorrection,//滾動偏移修正,當部位null或zero時,viewport會開始新一輪layout
double cacheExtent,//緩存範圍
})
複製代碼
上面介紹了一下二者屬性的意思,那如何根據輸入獲得產出,咱們須要看一個具體的實現(RenderSliverToBoxAdapter),咱們看他的performLayout方法
@override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
//佈局child獲取child的size,將SliverConstraint轉換成BoxConstraints,在滾動的方向範圍沒有限制
child.layout(constraints.asBoxConstraints(), parentUsesSize: true);
double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child.size.width;
break;
case Axis.vertical:
childExtent = child.size.height;
break;
}
assert(childExtent != null);
//計算它的繪製範圍
final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: childExtent);
//計算它的緩存範圍
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
//獲得SliverGeometry輸出
geometry = SliverGeometry(
scrollExtent: childExtent,//就是child的滾動內容大小
paintExtent: paintedChildSize,//child須要繪製的範圍
cacheExtent: cacheExtent,//緩存範圍
maxPaintExtent: childExtent,//最大繪製範圍,child的滾動內容大小
hitTestExtent: paintedChildSize,//命中測試範圍就是child繪製的範圍
hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,//是否有視覺溢出
);
//設置ChildParentData就是設置繪製偏移
setChildParentData(child, constraints, geometry);
}
double calculatePaintOffset(SliverConstraints constraints, { @required double from, @required double to }) {
assert(from <= to);
final double a = constraints.scrollOffset;
final double b = constraints.scrollOffset + constraints.remainingPaintExtent;
return (to.clamp(a, b) - from.clamp(a, b)).clamp(0.0, constraints.remainingPaintExtent);
}
void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) {
final SliverPhysicalParentData childParentData = child.parentData;
assert(constraints.axisDirection != null);
assert(constraints.growthDirection != null);
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
childParentData.paintOffset = Offset(0.0, -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)));
break;
case AxisDirection.right:
childParentData.paintOffset = Offset(-constraints.scrollOffset, 0.0);
break;
case AxisDirection.down:
childParentData.paintOffset = Offset(0.0, -constraints.scrollOffset);
break;
case AxisDirection.left:
childParentData.paintOffset = Offset(-(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)), 0.0);
break;
}
assert(childParentData.paintOffset != null);
}
複製代碼
child的SliverGeometry和繪製偏移都肯定了,那麼接下來就是繪製了,咱們看一下繪製。
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry.visible) {
final SliverPhysicalParentData childParentData = child.parentData;
context.paintChild(child, offset + childParentData.paintOffset);
}
}
複製代碼
就是簡單的加上偏移量再進行繪製。
從以上分析來看,整個滾動造成由一下步驟來實現