网站开发前端好还是后端好,深圳福田建网站,回忆网站怎么做,wordpress自定义查询Flutter框架中有很多滚动的Widget,ListView、GridView等#xff0c;这些Widget都是使用Scrollable配合Viewport来完成滚动的。我们来分析一下这个滚动效果是怎样实现的。 Scrollable在滚动中的作用 Scrollable继承自StatefulWidget#xff0c;我们看一下他的State的build方法…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继承自ViewportOffsetViewportOffset又继承自ChangeNotifierViewportOffset是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上一个activityScrollPositionWithSingleContext的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然后通知ListenersViewport在滚动中的作用 我们先看只包含一个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类型必须是RenderSliverrequired 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的scrollOffsetcenter的sliver的scrollOffset是centerOffsetscrollOffset - 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没有滚动则为idlerequired 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进行绘制转载于:https://juejin.im/post/5caec613f265da03a00fbcde