Flutter 小技巧之横竖列表的自适应大小布局支持

news2024/11/25 6:37:44

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

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

首先我们看下面这段代码,基本逻辑就是:我们希望 verticalListView 里每个 Item 都是根据内容自适应大小,并且 Item 会存在有 horizontalListView 这样的 child。

horizontalListView 我们也希望它能够根据自己的 children 去自适应大小。那么你觉得这段代码有什么问题?它能正常运行吗?


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 :https://gist.github.com/vejmartin/b8df4c94587bdad63f5b4ff111ff581c

首先我们基于 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 {
  
  Widget buildChildLayout(BuildContext context) {
    return UnboundedSliverList(delegate: childrenDelegate);
  }

  
  Widget buildViewport(
    BuildContext context,
    ViewportOffset offset,
    AxisDirection axisDirection,
    List<Widget> slivers,
  ) {
    return UnboundedViewport(
      axisDirection: axisDirection,
      offset: offset,
      slivers: slivers,
      cacheExtent: cacheExtent,
    );
  }

  
  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 {
  
  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 {
  
  bool get sizedByParent => false;

  double _unboundedSize = double.infinity;

  
  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;
    }
  }

  
  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 {
  
  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
  
  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 - 1, 0);
        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
  
  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 一个高度,这样的实现才是最优解

好了,本篇小技巧到这里就解决了,不知道对于类似实现,你是否还有什么想法,如果你有更好的解决方案,欢迎留言讨论。

完整代码可见:https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/un_bounded_listview.dart

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

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

相关文章

android studio EditText用法

1.自定义文本框 选中状态&#xff1a; <?xml version"1.0" encoding"utf-8"?> <shape xmlns:android"http://schemas.android.com/apk/res/android"><!--指定形状内部颜色--><solid android:color"#ffffff"&g…

机器学习在生态、环境经济学中的实践技术应用及论文写作

近年来&#xff0c;人工智能领域已经取得突破性进展&#xff0c;对经济社会各个领域都产生了重大影响&#xff0c;结合了统计学、数据科学和计算机科学的机器学习是人工智能的主流方向之一&#xff0c;目前也在飞快的融入计量经济学研究。表面上机器学习通常使用大数据&#xf…

点了下链接信息就泄露了,ta们是怎么做到的?

随着互联网的普及以及一系列可供上网设备的快速发展&#xff0c;截止2022年12月&#xff0c;中国网民规模达10.37亿&#xff0c;较之2021年12月增长3549万&#xff0c;互联网普及率达75.6%&#xff1b;在这么庞大的数据背后又有多少用户的个人信息被泄露呢? 一、信息泄露常见场…

2023 年最全面的 DevOps 工具列表,你用过几个?

在软件开发领域&#xff0c;DevOps已经成为越来越重要的概念。它强调了开发、测试、运维等各个环节之间的协作和自动化&#xff0c;以提高软件交付的速度和质量。随着时间的推移&#xff0c;DevOps所涉及的工具也不断更新和演进。本文将介绍一个预计在 2023 年最全面的 DevOps …

elementui中使用响应式布局实现五个盒子一行的适配

一、使用elementui中的自定义标签 自定义标签之后&#xff0c;浏览器中的css样式会出现这个类名 <el-row :gutter"30" class"row-bg"><el-col:xs"8":sm"6":md"4":lg"{ span: 24-5 }"class"headerC…

开发框架Furion之Winform+SqlSugar

目录 1.开发环境 2.项目搭建 2.1 创建WinFrom主项目 2.2 创建子项目 2.3 实体类库基础类信息配置 2.3.1 Nuget包及项目引用 2.3.2 实体基类创建 2.4 仓储业务类库基础配置 2.4.1 Nuget包及项目引用 2.4.2 Dtos实体 2.4.3 仓储基类 2.5 service注册类库基础配置 2…

【图形数据库】Neo4j简介及应用场景

文章目录 1.什么是Neo4j?2.图形数据结构3.Neo4j应用场景3.1我们可以将图领域划分成以下两部分&#xff1a;3.2目前&#xff0c;业内已经有了相对比较成熟的基于图数据库的解决方案&#xff0c;大致可以分为以下几类。3.2.1金融行业应用3.2.2社交网络图谱3.2.3企业关系图谱 总结…

Linux进程通信:存储映射mmap

1. 存储映射是什么&#xff1f; 如上图&#xff0c;存储映射是将块设备的文件映射到进程的虚拟地址空间。之后&#xff0c;进程可以直接使用指针操作其地址空间中映射的文件&#xff0c;对这块映射区操作就相当于操作文件。 2. 存储映射函数mmap的简单使用 &#xff08;1&…

网络安全岗位面试题大全:解析各个分支岗位的面试题目,帮助你上岸大厂

网络安全是一个广泛的领域&#xff0c;涵盖了许多不同的岗位和分支。我整理了网络安全各个岗位分支的面试题目&#xff1a; 安全工程师/系统管理员 您如何确保网络系统的安全性和保密性&#xff1f;您采用了哪些技术和工具&#xff1f;请描述一下您在过去工作中遇到的最具挑战…

C++ -5- 内存管理

文章目录 C语言和C内存管理的区别示例1. C/C 中程序内存区域划分2. C中动态内存管理3.operator new 与 operator delete 函数4.new 和 delete 的实现原理5.定位new表达式 C语言和C内存管理的区别示例 //C语言&#xff1a; struct SListNode {int data;struct SListNode* next; …

什么是内存?什么是内存逃逸?怎么做内存逃逸分析

内存 平时我们在电脑上听歌&#xff0c;聊天&#xff0c;或者启动某个程序&#xff0c;那么这个启动过程&#xff0c;其实就是把程序从硬盘读入到内存中去。就像安卓手机&#xff0c;内存不够了很卡&#xff0c;杀掉几个软件&#xff0c;内存就升上来了。但也不是所有的程序都…

产品经理需要了解api接口的哪些东西

一、作为产品经理&#xff0c;需要了解API接口的以下方面&#xff1a; 功能&#xff1a;API接口的功能是指它提供的业务功能&#xff0c;包括数据查询、修改、增加、删除、计算等等&#xff0c;根据产品的需求确定需要调用哪些API接口。请求方式和传参&#xff1a;API接口的请…

致力提供一站式数据可视化解决方案,支持报表、图表、大屏

一、开源项目简介 Davinci是一个DVAAS&#xff08;Data Visualization as a Service&#xff09;平台解决方案。 Davinci面向业务人员/数据工程师/数据分析师/数据科学家&#xff0c;致力于提供一站式数据可视化解决方案。既可作为公有云/私有云独立使用&#xff0c;也可作为…

Linux进程通信:信号

1. 信号的概念 Linux进程间通信的方式之一。信号也称为“软件中断”。 信号特点&#xff1a; 简单&#xff1b;携带信息有限&#xff1b;满足特定条件才发送信号&#xff1b;可进行用户空间和内核空间进程的交互&#xff1b; 2. 信号的编号 kill -l // 查看信号编号 POS…

ModelArts的使用

完整流程第一个实例&#xff1a;AI初学者&#xff1a;使用订阅算法构建模型实现花卉识别_AI开发平台ModelArts_最佳实践_模型训练&#xff08;预置算法-新版训练&#xff09;_华为云 一、支持的模型 可以在gitee上下载标准网络模型&#xff1a; models: Models of MindSpore …

Prometheus优化及高可用

Prometheus优化及高可用 概述 Prometheus几乎已成为监控领域的事实标准&#xff0c;它自带高效的时序数据库存储&#xff0c;可以让单台 Prometheus 能够高效的处理大量的数据&#xff0c;还有友好并且强大的 PromQL 语法&#xff0c;可以用来灵活的查询各种监控数据以及配置…

使用 chat_flutter 进行聊天记录展示

前言 最近需要实现一个聊天记录的页面展示&#xff0c;在网上发现没有适合自己的&#xff0c;于是自己就造了一个&#xff0c;总体感觉还不赖。 下面奉上地址、效果图和教程。 效果图 地址 github: https://github.com/xiaorui-23/chat_fluttergitee: https://gitee.com/xi…

搭建微型服务器(node express框架)

目录 一&#xff1a;打包&#xff08;npm run build&#xff09; 二&#xff1a;变成合法的包&#xff08;新建server文件夹&#xff09; 三&#xff1a;一路回车 四&#xff1a;新建服务器主文件 五&#xff1a;编辑server.js 六&#xff1a;node server启动服务器 七&a…

第十二章 享元模式

文章目录 前言一、享元模式基本介绍二、享元模式解决网站展现项目完整代码WebSite 抽象网站类User 外部状态用户内部状态网站 ConcreteWebSite网站工厂产生网站和负责共享&#xff08;池&#xff09; WebSiteFactoryClint 测试 三、享元模式在JDK-Interger的应用源码分析四、享…

NFS部署

共享/webdata/目录&#xff1b; ~ 用于存储 AppSrv 主机的 WEB 数据&#xff1b; ~ 仅允许 AppSrv 主机访问该共享&#xff1b; ~ 考虑安全&#xff0c;不论登入 NFS 的使用者身份为何&#xff0c;都将其设置为匿名用 户访问 StorageSrv和AppSrv nfs共享 1.安装nfs(App…