Flutter Stack下Positioned中ListView.builder无法完整渲染且不能用Expanded问题
问题根因与修复方案
- 你遇到的
Expanded/Flexible报错是正常现象,这两个组件只能在Row/Column/Flex这类弹性布局容器内使用,Stack属于绝对定位布局,本身不支持弹性布局属性,不需要用这两个组件包裹ListView。 - ListView被截断的核心原因有两个:
- 你获取的偏移是相对于全局屏幕的坐标,而
Positioned的top/left是相对于父级Stack的坐标,坐标不匹配导致ListView位置错位,部分区域超出Stack绘制范围被裁剪 - 操作按钮没有做定位,默认堆叠在页面左上角,遮挡了部分内容,同时Stack没有设置撑满约束,绘制范围受限
- 你获取的偏移是相对于全局屏幕的坐标,而
修复后完整代码
import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const GetPosition(), ); } } class GetPosition extends StatefulWidget { const GetPosition({Key? key}) : super(key: key); @override _GetPositionState createState() => _GetPositionState(); } class _GetPositionState extends State<GetPosition> { String changeText = 'dynamicaly changing text dynamicaly changing text dynamicaly changing text dynamicaly changing text dynamicaly changing text dynamicaly changing text'; final GlobalKey _anchorKey = GlobalKey(); final GlobalKey _stackKey = GlobalKey(); double _x = 0, _y = 0; void _updateOffset() { // 获取锚点元素的渲染对象 RenderBox? anchorBox = _anchorKey.currentContext?.findRenderObject() as RenderBox?; // 获取Stack的渲染对象 RenderBox? stackBox = _stackKey.currentContext?.findRenderObject() as RenderBox?; if (anchorBox == null || stackBox == null) return; // 把锚点的全局坐标转成相对于Stack的本地坐标 Offset anchorPosition = anchorBox.localToGlobal(Offset.zero); Offset stackPosition = stackBox.localToGlobal(Offset.zero); setState(() { _x = anchorPosition.dx - stackPosition.dx; // 加上锚点自身高度,让列表显示在锚点下方 _y = anchorPosition.dy - stackPosition.dy + anchorBox.size.height + 2; }); } @override Widget build(BuildContext context) { final List<String> entries = <String>[ "X: ${_x.toInt()}", "Y: ${_y.toStringAsFixed(1)}", 'Entry A', 'Entry B', 'Entry C', 'Entry D', 'Entry E' ]; return Scaffold( body: SafeArea( // 给Stack加key用来计算相对偏移,同时设置fit: StackFit.expand让Stack撑满父容器 child: Stack( key: _stackKey, fit: StackFit.expand, children: [ // 富文本内容加顶部padding,避免被操作按钮遮挡 Padding( padding: const EdgeInsets.only(top: 60), child: RichText( textAlign: TextAlign.left, text: TextSpan( text: changeText, style: const TextStyle(color: Colors.black, fontSize: 18.0), children: [ WidgetSpan( child: InkWell( splashFactory: NoSplash.splashFactory, key: _anchorKey, onTap: _updateOffset, child: const Text( " ListView.builder", style: TextStyle(color: Colors.red, fontSize: 18.0), ), ), ), TextSpan( text: changeText, style: const TextStyle(color: Colors.black, fontSize: 18.0), ), ], ), ), ), // 悬浮列表,层级高于富文本,不会被截断 if (_y > 0) Positioned( top: _y, left: _x, child: SizedBox( width: 310, height: 200, child: ListView.builder( padding: EdgeInsets.zero, itemCount: entries.length, itemBuilder: (BuildContext context, int index) { return ListTile( dense: true, title: Text( entries[index], style: const TextStyle(color: Colors.red, fontSize: 18.0), ), ); }), ), ), // 操作按钮固定在顶部,不会遮挡内容 Positioned( top: 0, left: 0, right: 0, child: Row( children: [ ElevatedButton( onPressed: () { changeText += ' add text dsds'; setState(() {}); // 文本更新后重新计算偏移 WidgetsBinding.instance.addPostFrameCallback((_) => _updateOffset()); }, child: const Text('添加文本')), const SizedBox(width: 8), ElevatedButton( onPressed: () { if (changeText.length > 10) { changeText = changeText.substring(10, changeText.length - 1); } else if (changeText.isNotEmpty) { changeText = changeText.substring(0, changeText.length - 1); } setState(() {}); WidgetsBinding.instance.addPostFrameCallback((_) => _updateOffset()); }, child: const Text('删除文本'), ) ], ), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { _updateOffset(); }, child: const Icon(Icons.calculate)), ); } }
额外优化点
- 点击锚点文本即可触发位置更新,无需额外点击悬浮按钮
- 文本增减后会自动重算锚点位置,列表会自动跟随锚点移动
- 增加了SafeArea适配状态栏,避免内容被系统状态栏遮挡
内容的提问来源于stack exchange,提问作者Jan
相关产品推荐
相关产品推荐

