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

Flutter Stack下Positioned中ListView.builder无法完整渲染且不能用Expanded问题

问题根因与修复方案

  1. 你遇到的Expanded/Flexible报错是正常现象,这两个组件只能在Row/Column/Flex这类弹性布局容器内使用,Stack属于绝对定位布局,本身不支持弹性布局属性,不需要用这两个组件包裹ListView。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 02:54:07