Flutter的滾動以及sliver約束

Flutter框架中有不少滾動的Widget,ListView、GridView等,這些Widget都是使用Scrollable配合Viewport來完成滾動的。咱們來分析一下這個滾動效果是怎樣實現的。ios

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來建立Viewportapp

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)。ide

咱們繼續看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方法ui

@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。this

看到這裏咱們能夠發現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進行繪製
相關文章
相關標籤/搜索