You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中TwoDimensionalScrollView无法实现SingleChildScrollView的滚动效果,且Row在maxXIndex为0的TwoDimensionalGridView内异常

Flutter中TwoDimensionalScrollView无法实现SingleChildScrollView的滚动效果,且Row在maxXIndex为0的TwoDimensionalGridView内异常

我最近在研究Flutter里的新TwoDimensionalScrollView类,发现目前几乎没有可用的示例参考。好不容易找到一段实现代码,但运行起来总是报RenderFlex溢出错误。我的需求其实很简单:想给TwoDimensionalScrollView设置固定宽高,把它嵌套在其他组件里,让这个小区域能像SingleChildScrollView那样,内部内容超出时可以横向/纵向滚动,但肯定是哪里没搞对——直接把下面的代码当main.dart运行就能看到错误,麻烦帮忙看看!

import 'dart:ui';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: false),
      home: Scaffold(
        body: SLTTwoDimensionalScrollView(
          mainAxis: Axis.vertical,
          verticalDetails: const ScrollableDetails.vertical(),
          horizontalDetails: const ScrollableDetails.horizontal(),
          diagonalDragBehavior: DiagonalDragBehavior.none,
          clipBehavior: Clip.hardEdge,
          width: 1500,
          height: 34,
          delegate: TwoDimensionalChildBuilderDelegate(
            maxXIndex: 0,
            maxYIndex: 500,
            builder: (BuildContext context, ChildVicinity vicinity) {
              return TextButton(
                onPressed: () {},
                child: Container(
                  width: 1500,
                  child: Row(
                    children: [
                      Container(
                        height: 34,
                        width: 500,
                        child: const Text("Filename"),
                      ),
                      Container(
                        height: 34,
                        width: 500,
                        child: const Text("Description"),
                      ),
                      Container(
                        height: 34,
                        width: 500,
                        child: const Text(
                          "FilePath is the coolest thing we got today and it's good"),
                      ),
                    ],
                  ),
                ),
              );
            }),
        ),
      ),
    );
  }
}

class SLTTwoDimensionalScrollView extends TwoDimensionalScrollView {
  SLTTwoDimensionalScrollView({
    super.key,
    super.primary,
    super.mainAxis = Axis.vertical,
    super.verticalDetails = const ScrollableDetails.vertical(),
    super.horizontalDetails = const ScrollableDetails.horizontal(),
    required TwoDimensionalChildBuilderDelegate delegate,
    super.cacheExtent,
    super.diagonalDragBehavior = DiagonalDragBehavior.none,
    super.dragStartBehavior = DragStartBehavior.start,
    super.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
    super.clipBehavior = Clip.hardEdge,
    this.height = 34,
    this.width = 500,
  }) : super(delegate: delegate);

  double height;
  double width;

  @override
  Widget buildViewport(BuildContext context, ViewportOffset verticalOffset,
      ViewportOffset horizontalOffset) {
    return SimpleBuilderTableViewport(
      horizontalOffset: horizontalOffset,
      horizontalAxisDirection: horizontalDetails.direction,
      verticalOffset: verticalOffset,
      verticalAxisDirection: verticalDetails.direction,
      mainAxis: mainAxis,
      delegate: delegate as TwoDimensionalChildBuilderDelegate,
      cacheExtent: cacheExtent,
      clipBehavior: clipBehavior,
      height: height,
      width: width,
    );
  }
}

class SimpleBuilderTableViewport extends TwoDimensionalViewport {
  SimpleBuilderTableViewport({
    super.key,
    required super.verticalOffset,
    required super.verticalAxisDirection,
    required super.horizontalOffset,
    required super.horizontalAxisDirection,
    required TwoDimensionalChildBuilderDelegate delegate,
    required super.mainAxis,
    super.cacheExtent,
    super.clipBehavior = Clip.hardEdge,
    this.height = 34,
    this.width = 500,
  }) : super(delegate: delegate);

  double height;
  double width;

  @override
  RenderTwoDimensionalViewport createRenderObject(BuildContext context) {
    return RenderSimpleBuilderTableViewport(
      horizontalOffset: horizontalOffset,
      horizontalAxisDirection: horizontalAxisDirection,
      verticalOffset: verticalOffset,
      verticalAxisDirection: verticalAxisDirection,
      mainAxis: mainAxis,
      delegate: delegate as TwoDimensionalChildBuilderDelegate,
      childManager: context as TwoDimensionalChildManager,
      cacheExtent: cacheExtent,
      clipBehavior: clipBehavior,
      height: height,
      width: width,
    );
  }

  @override
  void updateRenderObject(
      BuildContext context, RenderSimpleBuilderTableViewport renderObject) {
    renderObject
      ..horizontalOffset = horizontalOffset
      ..horizontalAxisDirection = horizontalAxisDirection
      ..verticalOffset = verticalOffset
      ..verticalAxisDirection = verticalAxisDirection
      ..mainAxis = mainAxis
      ..delegate = delegate
      ..cacheExtent = cacheExtent
      ..clipBehavior = clipBehavior
      ..height = height
      ..width = width;
  }
}

class RenderSimpleBuilderTableViewport extends RenderTwoDimensionalViewport {
  RenderSimpleBuilderTableViewport({
    required super.horizontalOffset,
    required super.horizontalAxisDirection,
    required super.verticalOffset,
    required super.verticalAxisDirection,
    required TwoDimensionalChildBuilderDelegate delegate,
    required super.mainAxis,
    required super.childManager,
    super.cacheExtent,
    super.clipBehavior = Clip.hardEdge,
    this.height = 34,
    this.width = 500,
  }) : super(delegate: delegate);

  RenderBox? testGetChildFor(ChildVicinity vicinity) => getChildFor(vicinity);

  double height;
  double width;

  @override
  void layoutChildSequence() {
    final double horizontalPixels = horizontalOffset.pixels;
    final double verticalPixels = verticalOffset.pixels;
    final double viewportWidth = viewportDimension.width + cacheExtent;
    final double viewportHeight = viewportDimension.height + cacheExtent;

    final TwoDimensionalChildBuilderDelegate builderDelegate =
        delegate as TwoDimensionalChildBuilderDelegate;
    final int maxRowIndex = builderDelegate.maxYIndex ?? 5;
    final int maxColumnIndex = builderDelegate.maxXIndex ?? 5;

    final int leadingColumn = math.max((horizontalPixels / width).floor(), 0);
    final int leadingRow = math.max((verticalPixels / height).floor(), 0);

    final int trailingColumn = math.min(
      ((horizontalPixels + viewportWidth) / width).ceil(),
      maxColumnIndex,
    );
    final int trailingRow = math.min(
      ((verticalPixels + viewportHeight) / height).ceil(),
      maxRowIndex,
    );

    double xLayoutOffset = (leadingColumn * width) - horizontalOffset.pixels;
    for (int column = leadingColumn; column <= trailingColumn; column++) {
      double yLayoutOffset = (leadingRow * height) - verticalOffset.pixels;
      for (int row = leadingRow; row <= trailingRow; row++) {
        final ChildVicinity vicinity =
            ChildVicinity(xIndex: column, yIndex: row);
        final RenderBox child = buildOrObtainChildFor(vicinity)!;
        child.layout(constraints.tighten(width: width, height: height));
        parentDataOf(child).layoutOffset = Offset(xLayoutOffset, yLayoutOffset);
        yLayoutOffset += height;
      }
      xLayoutOffset += width;
    }

    final double verticalExtent = height * (maxRowIndex + 1);
    verticalOffset.applyContentDimensions(
      0.0,
      clampDouble(
        verticalExtent - viewportDimension.height, 0.0, double.infinity),
    );

    final double horizontalExtent = width * (maxColumnIndex + 1);
    horizontalOffset.applyContentDimensions(
      0.0,
      clampDouble(
        horizontalExtent - viewportDimension.width, 0.0, double.infinity),
    );
  }
}

我本来以为只要像SingleChildScrollView那样设置宽高,内部内容超出时就会自动滚动,但实际完全不是这么回事。后来我还发现,当TwoDimensionalGridView的maxXIndex设为0时,里面的Row组件也会出现异常。


兄弟,我帮你梳理下问题根源和解决办法:

一、RenderFlex溢出的核心原因

你自定义的RenderSimpleBuilderTableViewport里,给每个子组件强制设置了固定宽高(width:1500、height:34),但第三个Container里的文本太长,而且没有设置换行或溢出处理——默认情况下Text会强制单行显示,直接撑破了Row的宽度约束,导致溢出错误。

另外,TwoDimensionalScrollView的设计初衷是二维网格滚动(类似表格、网格列表),而你把整个Row塞进了一个单元格里,这种用法其实违背了它的设计逻辑,容易出现约束冲突。

二、无法实现SingleChildScrollView式滚动的原因

SingleChildScrollView是为单个子组件的滚动设计的,而TwoDimensionalScrollView是基于行列单元格的滚动容器,两者的布局逻辑完全不同。你想用TwoDimensionalScrollView实现单组件滚动,相当于用错了工具,反而绕了弯路。

三、解决方案

1. 优先用嵌套SingleChildScrollView实现需求

如果只是需要一个局部的二维滚动区域,直接用两个SingleChildScrollView嵌套(横向+纵向),简单直接,完全符合你的需求:

// 替换原SLTTwoDimensionalScrollView的代码
SingleChildScrollView(
  scrollDirection: Axis.vertical,
  child: SingleChildScrollView(
    scrollDirection: Axis.horizontal,
    child: Container(
      width: 1500, // 固定宽度
      child: Column(
        children: List.generate(501, (index) {
          return TextButton(
            onPressed: () {},
            child: SizedBox(
              height: 34,
              child: Row(
                children: [
                  SizedBox(
                    width: 500,
                    child: const Text("Filename"),
                  ),
                  SizedBox(
                    width: 500,
                    child: const Text("Description"),
                  ),
                  SizedBox(
                    width: 500,
                    child: const Text(
                      "FilePath is the coolest thing we got today and it's good",
                      softWrap: true, // 允许换行,避免溢出
                      // 或者用下面的方式截断文本
                      // maxLines: 1,
                      // overflow: TextOverflow.ellipsis,
                    ),
                  ),
                ],
              ),
            ),
          );
        }),
      ),
    ),
  ),
)

2. 非要用TwoDimensionalScrollView的调整方案

如果一定要基于TwoDimensionalScrollView修改,需要做两处关键调整:

  • 给过长的Text添加换行/溢出处理,解决RenderFlex问题;
  • 修改RenderSimpleBuilderTableViewport的滚动范围计算逻辑,不再按单元格数量计算,而是直接取子组件的实际宽高作为滚动边界(但这样其实浪费了TwoDimensionalScrollView的网格特性)。

四、关于maxXIndex为0时Row异常的问题

当maxXIndex:0,意味着横向只有一列,你的自定义Viewport会给每个子组件设置固定宽度1500,但Row内部的三个Container总宽度也是1500,这时候只要给Text添加softWrap: true或者maxLines:1, overflow: TextOverflow.ellipsis,就能解决Row的溢出问题,同时确保约束正确传递。

总结一下:如果只是局部二维滚动,嵌套SingleChildScrollView是最优解;如果是做网格类场景,再去深入研究TwoDimensionalScrollView的正确用法,它不是用来替代SingleChildScrollView的哦~

备注:内容来源于stack exchange,提问作者JerContact

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.21 12:53:10