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的弹性滑动调整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的主要作用了

  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的约束去布局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);
  }
}
复制代码

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

总结

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

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

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

你可能感兴趣的:(移动开发)