Flutter框架中有很多滚动的Widget,ListView、GridView等,这些Widget都是使用Scrollable配合Viewport来完成滚动的。我们来分析一下这个滚动效果是怎样实现的。
Scrollable在滚动中的作用
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来创建Viewport
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)。
我们继续看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方法
@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。
看到这里我们可以发现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的弹性滑动调整offsetif (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);//发送UserScrollNotificationsetPixels(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);//计算出overscrollassert(() {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发送OverscrollNotificationreturn 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的主要作用了
- 监听用户手势,计算转换出各种滚动情况,并进行通知
- 计算滚动的pixels,然后通知Listeners
Viewport在滚动中的作用
我们先看只包含一个Child的Viewport
_RenderSingleChildViewport单一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的约束去布局childsize = 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多个child的Viewport
我们上面知道RenderViewport在offset改变时会重新去布局绘制,因为在RenderViewport重写了sizedByParent,那么它自身的size是在performResize中确定的,我们先看performResize
@override
void performResize() {size = constraints.biggest;//确定自己的size为约束的最大范围switch (axis) {case Axis.vertical:offset.applyViewportDimension(size.height);//赋值ViewportDimensionbreak;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的滚动偏移为0final double sliverScrollOffset = scrollOffset <= 0.0 ? 0.0 : scrollOffset;final double correctedCacheOrigin = math.max(cacheOrigin, -sliverScrollOffset);final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin;//创建SliverConstraints去布局childchild.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所用掉的cacheExtentremainingCacheExtent -= childLayoutGeometry.cacheExtent - cacheExtentCorrection;//计算下一个sliver的缓存起始cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0);}updateOutOfBandData(growthDirection, childLayoutGeometry);布局下一个sliverchild = advance(child);}//正确完成布局直接返回0return 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运用的绘制偏移中,来定位每一个slivercontext.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自行去处理。
SliverConstraints以及SliverGeometry
这两个是相对出现了,跟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会开始新一轮layoutdouble 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);}
}
复制代码
就是简单的加上偏移量再进行绘制。
总结
从以上分析来看,整个滚动形成由一下步骤来实现
- Scrollable监听用户手势,通知viewport内容已经发生偏移
- viewport通过偏移值,去计算每个SliverConstraints来得到每个sliver的SliverGeometry,然后根据SliverGeometry对sliver进行大小、位置的确定并绘制
- 最后sliver根据布局阶段计算出来的自己的滚动偏移量来对child进行绘制