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的弹性滑动调整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的主要作用了
- 监听用户手势,计算转换出各种滚动情况,并进行通知
- 计算滚动的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的约束去布局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多个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);//赋值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自行去处理。
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会开始新一轮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);
}
}
复制代码
就是简单的加上偏移量再进行绘制。
总结
从以上分析来看,整个滚动形成由一下步骤来实现
- Scrollable监听用户手势,通知viewport内容已经发生偏移
- viewport通过偏移值,去计算每个SliverConstraints来得到每个sliver的SliverGeometry,然后根据SliverGeometry对sliver进行大小、位置的确定并绘制
- 最后sliver根据布局阶段计算出来的自己的滚动偏移量来对child进行绘制