Flutter的滚动以及sliver约束

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的主要作用了

  1. 监听用户手势,计算转换出各种滚动情况,并进行通知
  2. 计算滚动的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);}
}
复制代码

就是简单的加上偏移量再进行绘制。

总结

从以上分析来看,整个滚动形成由一下步骤来实现

  1. Scrollable监听用户手势,通知viewport内容已经发生偏移
  2. viewport通过偏移值,去计算每个SliverConstraints来得到每个sliver的SliverGeometry,然后根据SliverGeometry对sliver进行大小、位置的确定并绘制
  3. 最后sliver根据布局阶段计算出来的自己的滚动偏移量来对child进行绘制

转载于:https://juejin.im/post/5caec613f265da03a00fbcde

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/536544.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

页面增加html,为静态页面HTML增加session功能

一般来说&#xff0c;只有服务器端的CGI程序(ASP、PHP、JSP)具有session会话功能&#xff0c;用来保存用户在网站期间(会话)的活动数据信息&#xff0c;而对于数量众多的静态页面(HTML)来说&#xff0c;只能使用客户端的cookies来保存临时活动数据&#xff0c;但对于cookies的操…

关于Istio 1.1,你所不知道的细节

本文整理自Istio社区成员Star在 Cloud Native Days China 2019 北京站的现场分享 第1则 主角 Istio Istio作为service mesh领域的明星项目&#xff0c;从2016年发布到现在热度不断攀升。 Istio & Envoy Github Star Growth 官网中Istio1.1的架构图除了数据面的Envoy和控制面…

html调用父页面的函数,js调用父框架函数与弹窗调用父页面函数的方法

调用父级中的 aaa的函数子页面中:οnclick"window.parent.frames.aaa()"父页面中:function aaa(){alert(‘bbbbb’);}----------------------------------------------frame框架里的页面要改其他同框架下的页面或父框架的页面就用parentwindow.opener引用的是window.…

读卡距离和信号强度两方面来考虑

选择物联宇手持终端机的时候&#xff0c;你可以参考以下几个原则&#xff1a;选择行业需要应用功能&#xff0c;能有效控制好预算。屏幕界面需要高清晰的&#xff0c;选用分辨率较高的能更好的支持展现。按照项目所需求的来分析&#xff0c;需要从读卡距离和信号强度两方面来考…

html script 放置位置,script标签应该放在HTML哪里,总结分享

几年前&#xff0c;有经验的程序员总是让我们将很明显&#xff0c;现在浏览器有了更加酷的兼容方式&#xff0c;这篇文章&#xff0c;俺将跟大家一起来学习script标签的async和defer新特性&#xff0c;探讨script应该放在哪里更好。页面加载方式在我们讨论当浏览器加载带有获取…

2021吉林高考26日几点可以查询成绩,2021吉林高考成绩查分时间及入口

2021吉林高考成绩查分时间及入口2021吉林高考成绩查分时间及入口&#xff0c;有一些高考生真的很积极&#xff0c;考完试当天就将答案给对好了&#xff0c;考试嘛&#xff0c;站在旁观者的角度来看总是有人欢喜有人忧。估出来分数不咋地的&#xff0c;整个六月就毁了。2021吉林…

easyui,layui和 vuejs 有什么区别

2019独角兽企业重金招聘Python工程师标准>>> easyui是功能强大但是有很多的组件使用功能是十分强大的&#xff0c;而layui是2016年才出来的前端框架&#xff0c;现在才更新到2.x版本还有很多的功能没有完善&#xff0c;也还存在一些不稳定的情况&#xff0c;但是lay…

广东2021高考成绩位次查询,广东一分一段表查询2021-广东省2021年一分一段统计表...

广东省高考一分一段表是同学们在填报高考志愿时的重要参考资料之一。根据一分一段表&#xff0c;大家不仅可以清楚地了解自己的高考成绩在全省的排名&#xff0c;还可以结合心仪的大学近3年在广东省的录取位次变化&#xff0c;判断出自己被录取的概率大概是多少。根据考试院公布…

bootstrap-select动态生成数据,设置默认选项(默认值)

bootstrap-select设置选中的属性是selected"selected"&#xff0c;只要找出哪一项要设置为默认选项&#xff0c;再设置其属性selected"selected"即可&#xff0c;亲测有效。代码如下&#xff1a; var currentId $(_this).attr(data_id);//目标id&#xff…

无法显示验证码去掉html,如何去除验证码-模版风格-易通免费企业网站系统 - Powered by CmsEasy...

去除前台用户登录验证码1.修改lib/default/user_act.php 注释掉或删除55-58行修改为//if(!session::get(verify) || front::post(verify)<>session::get(verify)) {//front::flash(验证码错误&#xff01;);//return;//} 复制代码2.修改template/default/user/login.html…

webpack4打包工具

什么是webpack webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时&#xff0c;它会递归地构建一个依赖关系图(dependency graph)&#xff0c;其中包含应用程序需要的每个模块&#xff0c;然后将所有这些模块打包成一个或多个…

通过计算机网络进行的商务活动包括,电子商务练习题及答案

“电子商务”练习题一、填空题1&#xff0e;EDI系统构成三要素包括数据标准化、(EDI软件及硬件)和(通信网络)。2.B2C电子商务模式主要有&#xff1a;门户网站、(电子零售商)、(内容提供商)、(交易经纪人)和社区服务商。3. 影响消费者网上购物的因素&#xff1a;商品特性、(商品…

PAKDD 2019 都有哪些重要看点?看这篇文章就够了!...

雷锋网 AI 科技评论按&#xff1a;亚太地区知识发现与数据挖掘国际会议&#xff08;Pacific Asia Knowledge Discovery and Data Mining&#xff0c;PAKDD&#xff09;是亚太地区数据挖掘领域的顶级国际会议&#xff0c;旨在为数据挖掘相关领域的研究者和从业者提供一个可自由 …

「javaScript-每三位插入一个逗号实现方式」

一道火了很久的面试题&#xff0c;//将以下数字从小数点前开始每三位数加一个逗号var num 1234567890.12345;复制代码相信大家写了这么久的前端代码&#xff0c;不论是培训也好&#xff0c;面试也好&#xff0c;这种题出现的频率挺高的&#xff0c;网上方法很多&#xff0c;但…

计算机网络df例题,计算机网络期末试题北交.doc

计算机网络期末试题北交北京交通大学 2007-2008学年 第学期考试试题课程名称&#xff1a;计算机通信与网络技术 出题人&#xff1a;网络课程组题 号一二三五总分得 分签 字选择题(每题分&#xff0c;共0分)PING命令使用协议的报文A、TCP ?? ?B、UDP ??????????C、…

java B2B2C 仿淘宝电子商城系统-Spring Cloud Feign的文件上传实现

在Spring Cloud封装的Feign中并不直接支持传文件&#xff0c;但可以通过引入Feign的扩展包来实现&#xff0c;本文就来具体说说如何实现。需要JAVA Spring Cloud大型企业分布式微服务云构建的B2B2C电子商务平台源码 一零三八七七四六二六 服务提供方&#xff08;接收文件&#…

2021计算机三级网络技术教程,全国计算机等级考试三级教程——网络技术(2021年版)...

前辅文第一单元 网络规划与设计第1章 网络系统结构与设计的基本原则1.1 基础知识1.2 实训任务习题第2章 中小型网络系统总体规划与设计方法2.1 基础知识2.2 实训任务习题第3章 IP地址规划设计技术3.1 基础知识3.2 实训任务习题第4章 路由设计基础4.1 基础知识4.2 实训任务习题第…

subline Text3 插件安装

--没有解决&#xff0c;换了vscode 安装Package Control 这是必须的步骤&#xff0c;安装任何插件之前需要安装这个 自动安装的方法最方便&#xff0c;只需要在控制台&#xff08;不是win的控制台&#xff0c;而是subline 的&#xff09;里粘贴一段代码就好&#xff0c;但是由…

大学计算机基础书本里的毕业论文源稿,计算机基础毕业论文范文

计算机基础毕业论文范文导语&#xff1a;关于大学计算机基础的教学&#xff0c;需要不断探索与实践&#xff0c;实现更好的教学。下面是小编带来的计算机基础毕业论文&#xff0c;欢迎阅读与参考。论文&#xff1a;大学计算机基础教学的探索与实践摘要&#xff1a;大学计算机基…

p批处理替换目录下文本中的字符串

echo offrem 进入批处理文件所在的路径 cd C:\Users\zxh\Desktop\123echo ***** Replace "123" as "abc" ***** rem 定义要替换的新旧字符串 set strOld123 set strNewabcrem 定义变量修改本地化延期 setlocal enabledelayedexpansionrem 循环取出要处理的…