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

