今日这个主题看着是不是有点抽象?又是列表嵌套?之前不是分享过《 ListView 和 PageView 的各种花式嵌套》了么?那这次的自适应巨细布局支撑有什么不同?

算是某些奇特的场景下才会需求。

首要咱们看下面这段代码,根本逻辑便是:咱们期望 verticalListView 里每个 Item 都是依据内容自适应巨细,而且 Item 会存在有 horizontalListView 这样的 child。

horizontalListView 咱们也期望它能够依据自己的 children 去自适应巨细。那么你觉得这段代码有什么问题?它能正常运转吗?

@override
Widget build(BuildContext context) {
 return Scaffold(
   appBar: AppBar(
     title: new Text(""),
  ),
   extendBody: true,
   body: Container(
     color: Colors.white,
     child: ListView(
       children: [
         ListView(
           scrollDirection: Axis.horizontal,
           children: List<Widget>.generate(50, (index) {
             return Padding(
               padding: EdgeInsets.all(2),
               child: Container(
                 color: Colors.blue,
                 child: Text(List.generate(
                         math.Random().nextInt(10), (index) => "TEST\n")
                    .toString()),
              ),
            );
          }),
        ),
         Container(
           height: 1000,
           color: Colors.green,
        ),
      ],
    ),
  ),
);
}

答案是不能,由于这段代码里 verticalListView 嵌套了 horizontalListView ,而横向的 ListView 并没有指定高度,而且垂直方向的 ListView 也没有指定 itemExtent ,所以咱们会得到如下图所示的错误:

为什么会有这样的问题,简略说一下,咱们都知道 Flutter 是从上往下传递束缚,从上往上回来 Size 的一个布局进程,也便是需求 child 经过经过 parent 的束缚来决议自己的巨细,然后 parent 依据 child 回来的 Size 决议自己的尺度。

对这部分感兴趣的能够看 《带你了解不一样的 Flutter》

可是关于可滑动控件来说有点特别,由于可滑动控件在其滑动方向的主轴上,理论是需求「无限大」的,所以关于可滑动控件来说,就需求有一个「窗口」的固定巨细,也便是 ViewPort 这个「窗口」需求有一个主轴方向的巨细。

比方 ListView ,一般情况下便是有一个 ViewPort ,然后内部的 SliverList 构建一个列表,然后经过手势ViewPort 「窗口」下相应发生移动,然后达到列表滑动的作用。

假如感兴趣能够看 《不一样视点带你了解 Flutter 中的滑动列表完成》

那么咱们再回到上面 verticalListView 嵌套 horizontalListView 的问题:

  • 由于垂直的 ListView 没有设置 itemExtent ,所以它的每个 child 不会有一个固定高度,由于咱们的需求是每个 Item 依据自己的需求自适应高度。
  • 横向的 ListView 没有设置清晰高度,作为 parent 的垂直 ListView 高度理论又是「无限高」,所以横向的 ListView 无法核算得到一个有效的高度。

别的,由于 ListView 不像 Row/Column等控件,它具有的 children 理论也是「无限」的,而且没有展现的部分一般是不会布局和绘制,所以不能像 Row/Column 一样核算出一切控件的高度之后,来决议自身的高度。

那么破解的办法有哪些呢?目前情况下能够提供两种处理办法。

SingleChildScrollView

如下代码所示,首要最简略的便是把横向的 ListView 替换成 SingleChildScrollView ,由于不同于 ListViewSingleChildScrollView 只有一个 child ,所以它的 ViewPort 也比较特别。

return Scaffold(
 appBar: AppBar(
   title: new Text("ControllerDemoPage"),
),
 extendBody: true,
 body: Container(
   color: Colors.white,
   child: ListView(
     children: [
       SingleChildScrollView(
         scrollDirection: Axis.horizontal,
         child: Row(
           children: List<Widget>.generate(50, (index) {
             return Padding(
               padding: EdgeInsets.all(2),
               child: Container(
                 color: Colors.blue,
                 child: Text(List.generate(
                         math.Random().nextInt(10), (index) => "TEST\n")
                    .toString()),
              ),
            );
          }),
        ),
      ),
       Container(
         height: 1000,
         color: Colors.green,
      ),
    ],
  ),
),
)

SingleChildScrollView_RenderSingleChildViewport 里,布局时能够很简略的经过 child!.layout 之后得到 child 的巨细,然后合作 Row 就核算出一切 child 的综合高度,这样能够完成横向的列表作用。

运转之后成果入下图所示,能够看到此刻在垂直的 ListView里,横向的 SingleChildScrollView 被正确渲染出来,可是此刻出现「良莠不齐」的高度布局。

如下代码所示,这时候咱们只需求在 Row 嵌套一个 IntrinsicHeight ,就能够让其内部高度对齐,由于 IntrinsicHeight 在布局时会提早调用 child 的 getMaxIntrinsicHeight 获取 child 的高度,修正 parent 传递给 child 的束缚信息。

SingleChildScrollView(
 scrollDirection: Axis.horizontal,
 child: IntrinsicHeight(
   child: Row(
     children: List<Widget>.generate(50, (index) {
       return Padding(
         padding: EdgeInsets.all(2),
         child: Container(
           alignment: Alignment.bottomCenter,
           color: Colors.blue,
           child: Text(List.generate(
                   math.Random().nextInt(10), (index) => "TEST\n")
              .toString()),
        ),
      );
    }),
  ),
),
),

运转作用如下所示,能够看到此刻一切横向 Item 的高度都共同,可是这个处理办法也有两个比较丧命的问题:

  • SingleChildScrollView 里是经过 Row 核算的高度,也便是布局时会需求一次性核算一切 child ,假如列表太长就会发生性能损耗
  • IntrinsicHeight 推算布局的进程会比较费时,可能会到 O(N²),虽然 Flutter 里针对这部分核算成果做了缓存,可是不妨碍它的耗时。

UnboundedListView

第二个处理思路便是根据 ListView 去自定义,前面咱们不是说 ListView 不会像 Row 那样去计算 children 的巨细么?那咱们完全能够自定义一个 UnboundedListView 来计算。

这部分思路最早来自 Github :gist.github.com/vejmartin/b…

首要咱们根据 ListView 定义一个 UnboundedListView ,经过 mixin 的办法override 对应的 ViewportSliver ,也便是:

  • buildChildLayout 里的 SliverList 替换成咱们自定义的 UnboundedSliverList
  • buildViewport 里的 Viewport 替换成咱们自定义的 UnboundedViewport
  • buildSlivers 里处理Padding 逻辑,把 SliverPadding 替换为自定义的 UnboundedSliverPadding
class UnboundedListView = ListView with UnboundedListViewMixin;
​
​
/// BoxScrollView 的基础上
mixin UnboundedListViewMixin on ListView {
 @override
 Widget buildChildLayout(BuildContext context) {
   return UnboundedSliverList(delegate: childrenDelegate);
}
​
 @protected
 Widget buildViewport(
   BuildContext context,
   ViewportOffset offset,
   AxisDirection axisDirection,
   List<Widget> slivers,
) {
   return UnboundedViewport(
     axisDirection: axisDirection,
     offset: offset,
     slivers: slivers,
     cacheExtent: cacheExtent,
  );
}
​
 @override
 List<Widget> buildSlivers(BuildContext context) {
   Widget sliver = buildChildLayout(context);
   EdgeInsetsGeometry? effectivePadding = padding;
   if (padding == null) {
     final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
     if (mediaQuery != null) {
       // Automatically pad sliver with padding from MediaQuery.
       final EdgeInsets mediaQueryHorizontalPadding =
           mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
       final EdgeInsets mediaQueryVerticalPadding =
           mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
       // Consume the main axis padding with SliverPadding.
       effectivePadding = scrollDirection == Axis.vertical
           ? mediaQueryVerticalPadding
          : mediaQueryHorizontalPadding;
       // Leave behind the cross axis padding.
       sliver = MediaQuery(
         data: mediaQuery.copyWith(
           padding: scrollDirection == Axis.vertical
               ? mediaQueryHorizontalPadding
              : mediaQueryVerticalPadding,
        ),
         child: sliver,
      );
    }
  }
​
   if (effectivePadding != null)
     sliver =
         UnboundedSliverPadding(padding: effectivePadding, sliver: sliver);
   return <Widget>[sliver];
}
}

接下来首要是完成 UnboundedViewport ,一样的套路:

  • 首要根据 Viewport 的基础上,经过 createRenderObjectRenderViewPort 修正为咱们的 UnboundedRenderViewport
  • 根据 RenderViewport 添加 performLayoutlayoutChildSequence 的自定义逻辑,实际上便是添加一个 unboundedSize 参数,这个参数经过 child 的 RenderSliver 里去计算得到
class UnboundedViewport = Viewport with UnboundedViewportMixin;
mixin UnboundedViewportMixin on Viewport {
 @override
 RenderViewport createRenderObject(BuildContext context) {
   return UnboundedRenderViewport(
     axisDirection: axisDirection,
     crossAxisDirection: crossAxisDirection ??
         Viewport.getDefaultCrossAxisDirection(context, axisDirection),
     anchor: anchor,
     offset: offset,
     cacheExtent: cacheExtent,
  );
}
}
​
class UnboundedRenderViewport = RenderViewport
   with UnboundedRenderViewportMixin;
mixin UnboundedRenderViewportMixin on RenderViewport {
 @override
 bool get sizedByParent => false;
​
 double _unboundedSize = double.infinity;
​
 @override
 void performLayout() {
   BoxConstraints constraints = this.constraints;
   if (axis == Axis.horizontal) {
     _unboundedSize = constraints.maxHeight;
     size = Size(constraints.maxWidth, 0);
  } else {
     _unboundedSize = constraints.maxWidth;
     size = Size(0, constraints.maxHeight);
  }
​
   super.performLayout();
​
   switch (axis) {
     case Axis.vertical:
       offset.applyViewportDimension(size.height);
       break;
     case Axis.horizontal:
       offset.applyViewportDimension(size.width);
       break;
  }
}
​
 @override
 double layoutChildSequence({
   required RenderSliver? child,
   required double scrollOffset,
   required double overlap,
   required double layoutOffset,
   required double remainingPaintExtent,
   required double mainAxisExtent,
   required double crossAxisExtent,
   required GrowthDirection growthDirection,
   required RenderSliver? advance(RenderSliver child),
   required double remainingCacheExtent,
   required double cacheOrigin,
}) {
   crossAxisExtent = _unboundedSize;
   var firstChild = child;
​
   final result = super.layoutChildSequence(
     child: child,
     scrollOffset: scrollOffset,
     overlap: overlap,
     layoutOffset: layoutOffset,
     remainingPaintExtent: remainingPaintExtent,
     mainAxisExtent: mainAxisExtent,
     crossAxisExtent: crossAxisExtent,
     growthDirection: growthDirection,
     advance: advance,
     remainingCacheExtent: remainingCacheExtent,
     cacheOrigin: cacheOrigin,
  );
​
   double unboundedSize = 0;
   while (firstChild != null) {
     if (firstChild.geometry is UnboundedSliverGeometry) {
       final UnboundedSliverGeometry childGeometry =
           firstChild.geometry as UnboundedSliverGeometry;
       unboundedSize = math.max(unboundedSize, childGeometry.crossAxisSize);
    }
     firstChild = advance(firstChild);
  }
   if (axis == Axis.horizontal) {
     size = Size(size.width, unboundedSize);
  } else {
     size = Size(unboundedSize, size.height);
  }
​
   return result;
}
}

接下来咱们承继 SliverGeometry 自定义一个 UnboundedSliverGeometry ,主要便是添加了一个 crossAxisSize 参数,用来记载当时计算到的副轴高度,然后让上面的 ViewPort 能够获取得到。

class UnboundedSliverGeometry extends SliverGeometry {
 UnboundedSliverGeometry(
    {SliverGeometry? existing, required this.crossAxisSize})
    : super(
         scrollExtent: existing?.scrollExtent ?? 0.0,
         paintExtent: existing?.paintExtent ?? 0.0,
         paintOrigin: existing?.paintOrigin ?? 0.0,
         layoutExtent: existing?.layoutExtent,
         maxPaintExtent: existing?.maxPaintExtent ?? 0.0,
         maxScrollObstructionExtent:
             existing?.maxScrollObstructionExtent ?? 0.0,
         hitTestExtent: existing?.hitTestExtent,
         visible: existing?.visible,
         hasVisualOverflow: existing?.hasVisualOverflow ?? false,
         scrollOffsetCorrection: existing?.scrollOffsetCorrection,
         cacheExtent: existing?.cacheExtent,
      );
​
 final double crossAxisSize;
}

如下代码所示,终究咱们根据 SliverList 完成一个 UnboundedSliverList ,这也是核心逻辑,主要是完成 performLayout 部分的代码,咱们需求在本来代码的基础上,在某些节点加上自定义的逻辑,用于计算参与布局的每个 Item 的高度,然后得到一个最大值。

代码看起来很长,可是其实咱们新增的很少。

class UnboundedSliverList = SliverList with UnboundedSliverListMixin;
mixin UnboundedSliverListMixin on SliverList {
 @override
 RenderSliverList createRenderObject(BuildContext context) {
   final SliverMultiBoxAdaptorElement element =
       context as SliverMultiBoxAdaptorElement;
   return UnboundedRenderSliverList(childManager: element);
}
}
​
class UnboundedRenderSliverList extends RenderSliverList {
 UnboundedRenderSliverList({
   required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
​
 // See RenderSliverList::performLayout
 @override
 void performLayout() {
   final SliverConstraints constraints = this.constraints;
   childManager.didStartLayout();
   childManager.setDidUnderflow(false);
​
   final double scrollOffset =
       constraints.scrollOffset + constraints.cacheOrigin;
   assert(scrollOffset >= 0.0);
   final double remainingExtent = constraints.remainingCacheExtent;
   assert(remainingExtent >= 0.0);
   final double targetEndScrollOffset = scrollOffset + remainingExtent;
   BoxConstraints childConstraints = constraints.asBoxConstraints();
   int leadingGarbage = 0;
   int trailingGarbage = 0;
   bool reachedEnd = false;
​
   if (constraints.axis == Axis.horizontal) {
     childConstraints = childConstraints.copyWith(minHeight: 0);
  } else {
     childConstraints = childConstraints.copyWith(minWidth: 0);
  }
​
   double unboundedSize = 0;
​
   // should call update after each child is laid out
   updateUnboundedSize(RenderBox? child) {
     if (child == null) {
       return;
    }
     unboundedSize = math.max(
         unboundedSize,
         constraints.axis == Axis.horizontal
             ? child.size.height
            : child.size.width);
  }
​
   unboundedGeometry(SliverGeometry geometry) {
     return UnboundedSliverGeometry(
       existing: geometry,
       crossAxisSize: unboundedSize,
    );
  }
​
   // This algorithm in principle is straight-forward: find the first child
   // that overlaps the given scrollOffset, creating more children at the top
   // of the list if necessary, then walk down the list updating and laying out
   // each child and adding more at the end if necessary until we have enough
   // children to cover the entire viewport.
   //
   // It is complicated by one minor issue, which is that any time you update
   // or create a child, it's possible that the some of the children that
   // haven't yet been laid out will be removed, leaving the list in an
   // inconsistent state, and requiring that missing nodes be recreated.
   //
   // To keep this mess tractable, this algorithm starts from what is currently
   // the first child, if any, and then walks up and/or down from there, so
   // that the nodes that might get removed are always at the edges of what has
   // already been laid out.
​
   // Make sure we have at least one child to start from.
   if (firstChild == null) {
     if (!addInitialChild()) {
       // There are no children.
       geometry = unboundedGeometry(SliverGeometry.zero);
       childManager.didFinishLayout();
       return;
    }
  }
​
   // We have at least one child.
​
   // These variables track the range of children that we have laid out. Within
   // this range, the children have consecutive indices. Outside this range,
   // it's possible for a child to get removed without notice.
   RenderBox? leadingChildWithLayout, trailingChildWithLayout;
​
   RenderBox? earliestUsefulChild = firstChild;
​
   // A firstChild with null layout offset is likely a result of children
   // reordering.
   //
   // We rely on firstChild to have accurate layout offset. In the case of null
   // layout offset, we have to find the first child that has valid layout
   // offset.
   if (childScrollOffset(firstChild!) == null) {
     int leadingChildrenWithoutLayoutOffset = 0;
     while (earliestUsefulChild != null &&
         childScrollOffset(earliestUsefulChild) == null) {
       earliestUsefulChild = childAfter(earliestUsefulChild);
       leadingChildrenWithoutLayoutOffset += 1;
    }
     // We should be able to destroy children with null layout offset safely,
     // because they are likely outside of viewport
     collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
     // If can not find a valid layout offset, start from the initial child.
     if (firstChild == null) {
       if (!addInitialChild()) {
         // There are no children.
         geometry = unboundedGeometry(SliverGeometry.zero);
         childManager.didFinishLayout();
         return;
      }
    }
  }
​
   // Find the last child that is at or before the scrollOffset.
   earliestUsefulChild = firstChild;
   for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
       earliestScrollOffset > scrollOffset;
       earliestScrollOffset = childScrollOffset(earliestUsefulChild)!) {
     // We have to add children before the earliestUsefulChild.
     earliestUsefulChild =
         insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
     updateUnboundedSize(earliestUsefulChild);
     if (earliestUsefulChild == null) {
       final SliverMultiBoxAdaptorParentData childParentData =
           firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
       childParentData.layoutOffset = 0.0;
​
       if (scrollOffset == 0.0) {
         // insertAndLayoutLeadingChild only lays out the children before
         // firstChild. In this case, nothing has been laid out. We have
         // to lay out firstChild manually.
         firstChild!.layout(childConstraints, parentUsesSize: true);
         earliestUsefulChild = firstChild;
         updateUnboundedSize(earliestUsefulChild);
         leadingChildWithLayout = earliestUsefulChild;
         trailingChildWithLayout ??= earliestUsefulChild;
         break;
      } else {
         // We ran out of children before reaching the scroll offset.
         // We must inform our parent that this sliver cannot fulfill
         // its contract and that we need a scroll offset correction.
         geometry = unboundedGeometry(SliverGeometry(
           scrollOffsetCorrection: -scrollOffset,
        ));
         return;
      }
    }
​
     final double firstChildScrollOffset =
         earliestScrollOffset - paintExtentOf(firstChild!);
     // firstChildScrollOffset may contain double precision error
     if (firstChildScrollOffset < -precisionErrorTolerance) {
       // Let's assume there is no child before the first child. We will
       // correct it on the next layout if it is not.
       geometry = unboundedGeometry(SliverGeometry(
         scrollOffsetCorrection: -firstChildScrollOffset,
      ));
       final SliverMultiBoxAdaptorParentData childParentData =
           firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
       childParentData.layoutOffset = 0.0;
       return;
    }
​
     final SliverMultiBoxAdaptorParentData childParentData =
         earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
     childParentData.layoutOffset = firstChildScrollOffset;
     assert(earliestUsefulChild == firstChild);
     leadingChildWithLayout = earliestUsefulChild;
     trailingChildWithLayout ??= earliestUsefulChild;
  }
​
   assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
​
   // If the scroll offset is at zero, we should make sure we are
   // actually at the beginning of the list.
   if (scrollOffset < precisionErrorTolerance) {
     // We iterate from the firstChild in case the leading child has a 0 paint
     // extent.
     while (indexOf(firstChild!) > 0) {
       final double earliestScrollOffset = childScrollOffset(firstChild!)!;
       // We correct one child at a time. If there are more children before
       // the earliestUsefulChild, we will correct it once the scroll offset
       // reaches zero again.
       earliestUsefulChild =
           insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
       updateUnboundedSize(earliestUsefulChild);
       assert(earliestUsefulChild != null);
       final double firstChildScrollOffset =
           earliestScrollOffset - paintExtentOf(firstChild!);
       final SliverMultiBoxAdaptorParentData childParentData =
           firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
       childParentData.layoutOffset = 0.0;
       // We only need to correct if the leading child actually has a
       // paint extent.
       if (firstChildScrollOffset < -precisionErrorTolerance) {
         geometry = unboundedGeometry(SliverGeometry(
           scrollOffsetCorrection: -firstChildScrollOffset,
        ));
         return;
      }
    }
  }
​
   // At this point, earliestUsefulChild is the first child, and is a child
   // whose scrollOffset is at or before the scrollOffset, and
   // leadingChildWithLayout and trailingChildWithLayout are either null or
   // cover a range of render boxes that we have laid out with the first being
   // the same as earliestUsefulChild and the last being either at or after the
   // scroll offset.
​
   assert(earliestUsefulChild == firstChild);
   assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
​
   // Make sure we've laid out at least one child.
   if (leadingChildWithLayout == null) {
     earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
     updateUnboundedSize(earliestUsefulChild);
     leadingChildWithLayout = earliestUsefulChild;
     trailingChildWithLayout = earliestUsefulChild;
  }
​
   // Here, earliestUsefulChild is still the first child, it's got a
   // scrollOffset that is at or before our actual scrollOffset, and it has
   // been laid out, and is in fact our leadingChildWithLayout. It's possible
   // that some children beyond that one have also been laid out.
​
   bool inLayoutRange = true;
   RenderBox? child = earliestUsefulChild;
   int index = indexOf(child!);
   double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
   bool advance() {
     // returns true if we advanced, false if we have no more children
     // This function is used in two different places below, to avoid code duplication.
     assert(child != null);
     if (child == trailingChildWithLayout) inLayoutRange = false;
     child = childAfter(child!);
     if (child == null) inLayoutRange = false;
     index += 1;
     if (!inLayoutRange) {
       if (child == null || indexOf(child!) != index) {
         // We are missing a child. Insert it (and lay it out) if possible.
         child = insertAndLayoutChild(
           childConstraints,
           after: trailingChildWithLayout,
           parentUsesSize: true,
        );
         updateUnboundedSize(child);
         if (child == null) {
           // We have run out of children.
           return false;
        }
      } else {
         // Lay out the child.
         child!.layout(childConstraints, parentUsesSize: true);
         updateUnboundedSize(child!);
      }
       trailingChildWithLayout = child;
    }
     assert(child != null);
     final SliverMultiBoxAdaptorParentData childParentData =
         child!.parentData! as SliverMultiBoxAdaptorParentData;
     childParentData.layoutOffset = endScrollOffset;
     assert(childParentData.index == index);
     endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
     return true;
  }
​
   // Find the first child that ends after the scroll offset.
   while (endScrollOffset < scrollOffset) {
     leadingGarbage += 1;
     if (!advance()) {
       assert(leadingGarbage == childCount);
       assert(child == null);
       // we want to make sure we keep the last child around so we know the end scroll offset
       collectGarbage(leadingGarbage - 10);
       assert(firstChild == lastChild);
       final double extent =
           childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
       geometry = unboundedGeometry(
         SliverGeometry(
           scrollExtent: extent,
           paintExtent: 0.0,
           maxPaintExtent: extent,
        ),
      );
       return;
    }
  }
​
   // Now find the first child that ends after our end.
   while (endScrollOffset < targetEndScrollOffset) {
     if (!advance()) {
       reachedEnd = true;
       break;
    }
  }
​
   // Finally count up all the remaining children and label them as garbage.
   if (child != null) {
     child = childAfter(child!);
     while (child != null) {
       trailingGarbage += 1;
       child = childAfter(child!);
    }
  }
​
   // At this point everything should be good to go, we just have to clean up
   // the garbage and report the geometry.
​
   collectGarbage(leadingGarbage, trailingGarbage);
​
   assert(debugAssertChildListIsNonEmptyAndContiguous());
   double estimatedMaxScrollOffset;
   if (reachedEnd) {
     estimatedMaxScrollOffset = endScrollOffset;
  } else {
     estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
       constraints,
       firstIndex: indexOf(firstChild!),
       lastIndex: indexOf(lastChild!),
       leadingScrollOffset: childScrollOffset(firstChild!),
       trailingScrollOffset: endScrollOffset,
    );
     assert(estimatedMaxScrollOffset >=
         endScrollOffset - childScrollOffset(firstChild!)!);
  }
   final double paintExtent = calculatePaintOffset(
     constraints,
     from: childScrollOffset(firstChild!)!,
     to: endScrollOffset,
  );
   final double cacheExtent = calculateCacheOffset(
     constraints,
     from: childScrollOffset(firstChild!)!,
     to: endScrollOffset,
  );
   final double targetEndScrollOffsetForPaint =
       constraints.scrollOffset + constraints.remainingPaintExtent;
   geometry = unboundedGeometry(
     SliverGeometry(
       scrollExtent: estimatedMaxScrollOffset,
       paintExtent: paintExtent,
       cacheExtent: cacheExtent,
       maxPaintExtent: estimatedMaxScrollOffset,
       // Conservative to avoid flickering away the clip during scroll.
       hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint ||
           constraints.scrollOffset > 0.0,
    ),
  );
​
   // We may have started the layout while scrolled to the end, which would not
   // expose a new child.
   if (estimatedMaxScrollOffset == endScrollOffset)
     childManager.setDidUnderflow(true);
   childManager.didFinishLayout();
}
}

别看上面这段代码很长,其实许多都是 RenderSliverList 自己的源码,如下图所示,真实咱们修正添加的只有这么点:

  • 在开始前添加 updateUnboundedSizeunboundedGeometry 用于记载布局高度和生成 UnboundedSliverGeometry
  • 将一切本来的 SliverGeometry 修正为 UnboundedSliverGeometry
  • 在一切触及 layout 的方位后边调用 updateUnboundedSize ,由于 child 在布局之后咱们就能够获取到它的 Size ,然后咱们计算得到他们的最大值,就能够经过 UnboundedSliverGeometry 回来给 ViewPort

最后如下代码所示,将 UnboundedListView 添加到一开始的垂直 ListView里,运转之后能够看到,跟着横向滑动,列表的自身高度在发生变化。

return Scaffold(
 appBar: AppBar(
   title: new Text("ControllerDemoPage"),
),
 extendBody: true,
 body: Container(
   color: Colors.white,
   child: ListView(
     children: [
       SingleChildScrollView(
         scrollDirection: Axis.horizontal,
         child: IntrinsicHeight(
           child: Row(
             children: List<Widget>.generate(50, (index) {
               return Padding(
                 padding: EdgeInsets.all(2),
                 child: Container(
                   alignment: Alignment.bottomCenter,
                   color: Colors.blue,
                   child: Text(List.generate(
                           math.Random().nextInt(10), (index) => "TEST\n")
                      .toString()),
                ),
              );
            }),
          ),
        ),
      ),
       UnboundedListView.builder(
           scrollDirection: Axis.horizontal,
           itemCount: 100,
           itemBuilder: (context, index) {
             print('$index');
             return Padding(
               padding: EdgeInsets.all(2),
               child: Container(
                 height: index * 1.0 + 10,
                 width: 50,
                 color: Colors.blue,
              ),
            );
          }),
       Container(
         height: 1000,
         color: Colors.green,
      ),
    ],
  ),
),
);

那么这是否达到了咱们的需求?如下代码所示,假如我将代码修正成如下所示,运转之后能够看到,此刻的横向列表变成了良莠不齐的状况。

UnboundedListView.builder(
    scrollDirection: Axis.horizontal,
    itemCount: 100,
    itemBuilder: (context, index) {
      print('$index');
      return Container(
        padding: EdgeInsets.all(2),
        child: Container(
          width: 50,
          color: Colors.blue,
          alignment: Alignment.bottomCenter,
          child: Text(List.generate(
                  math.Random().nextInt(15), (index) => "TEST\n")
              .toString()),
        ),
      );
    }),

可是这时候咱们无法用类似 IntrinsicHeight 的办法来处理,由于 ListView 里的 Item 都是动态处理的,也便是布局时需求处理特定廉价规模内的 Item 添加和毁掉,详细在 performLayout 里会经过 scrollOffsettargetEndScrollOffset 等来确认布局 Item 的规模。

这样就导致咱们经过 firstChild 链表结构去拜访的时候,咱们无法在 layout 之前获取到 child ,由于此刻它还没有被 add 到链表里,同时也受限于 insertAndLayoutLeadingChildinsertAndLayoutChild 的耦合完成和私有办法限制,这儿不方便简略重写支撑。

可是「天无绝人之路」,既然咱们不能在 child layout 之前处理,那么咱们能够在 layout 之后做多一次冗余布局,如下代码所示:

  • 咱们首要将 unboundedSize 提取为 UnboundedRenderSliverList 里的全局变量
  • didFinishLayout 之前,经过 firstChild 链表结构,重新经过 layout(childConstraints.tighten(height: unboundedSize) 布局多一次
  double unboundedSize = 0;
  // See RenderSliverList::performLayout
  @override
  void performLayout() {
    ····
    var tmpChild = firstChild;
    while (tmpChild != null) {
      tmpChild.layout(childConstraints.tighten(height: unboundedSize),
          parentUsesSize: true);
      tmpChild = childAfter(tmpChild);
    }
    childManager.didFinishLayout();
    ····
  }

运转之后能够看到,此刻列表现已悉数对齐,而损耗便是 child 会有 double 布局的情况,关于此处性能损耗,比照 SingleChildScrollView 的完成,能够依据实际场景来取舍运用哪种逻辑,当然,为了性能考虑非必要仍是给横向 ListView 一个高度,这样的完成才是最优解

好了,本篇小技巧到这儿就处理了,不知道关于类似完成,你是否还有什么主意,假如你有更好的处理方案,欢迎留言评论。

完好代码可见:github.com/CarGuo/gsy_…