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

Flutter嵌套列表滑至子列表首尾触发父列表滚动,上滑不流畅求解

Flutter嵌套列表边界滚动衔接优化方案

核心问题

原实现仅处理了子列表向下滚动到底部时父列表衔接滚动的逻辑,缺少子列表向上滚动到顶部、父列表反向滚动回子列表区域的对应处理,同时存在重复添加监听器、硬编码滚动偏移的问题,导致上滑时衔接不流畅。

优化后完整代码

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Flutter Demo', home: MyListView());
  }
}

class MyListView extends StatelessWidget {
  final ScrollController _mainScrollController = ScrollController();
  final double listHeight = 370;

  MyListView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AppBar'),
      ),
      body: SizedBox(
        height: MediaQuery.of(context).size.height,
        child: ListView(
          controller: _mainScrollController,
          children: <Widget>[
            SizedBox(
                height: listHeight,
                child: RapportList(
                    parentScrollController: _mainScrollController)),
            const OtherElement(
              text: "Other element 1 which will be scrolled",
            ),
            const OtherElement(
              text: "Other element 2 which will be scrolled",
            ),
            const OtherElement(
              text: "Other element 3 which will be scrolled",
            ),
            const OtherElement(
              text: "Other element 4 which will be scrolled",
            ),
            const OtherElement(
              text: "Other element 5 which will be scrolled",
            ),
            SizedBox(
                height: listHeight,
                child: RapportList(
                    parentScrollController: _mainScrollController)),
            const OtherElement(
              text: "Other element 4 which will be scrolled",
            ),
            const OtherElement(
              text: "Other element 5 which will be scrolled",
            ),
            SizedBox(
                height: listHeight,
                child: RapportList(
                    parentScrollController: _mainScrollController)),
          ],
        ),
      ),
    );
  }
}

class RapportList extends StatefulWidget {
  final ScrollController parentScrollController;

  const RapportList({super.key, required this.parentScrollController});

  @override
  _RapportListState createState() => _RapportListState();
}

class _RapportListState extends State<RapportList> {
  ScrollPhysics physics = const ScrollPhysics();
  late ScrollController _listViewScrollController;

  void listViewScrollListener() {
    if (_listViewScrollController.position.outOfRange) return;
    // 子列表滑到底部
    if (_listViewScrollController.offset >=
        _listViewScrollController.position.maxScrollExtent) {
      setState(() {
        physics = const NeverScrollableScrollPhysics();
      });
    }
    // 子列表滑到顶部
    if (_listViewScrollController.offset <=
        _listViewScrollController.position.minScrollExtent) {
      setState(() {
        physics = const NeverScrollableScrollPhysics();
      });
    }
  }

  void mainScrollListener() {
    if (widget.parentScrollController.position.outOfRange) return;
    // 父列表滚动时,判断当前滚动位置是否在子列表可视范围内
    final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
    if (renderBox == null) return;
    final position = renderBox.localToGlobal(Offset.zero);
    // 子列表进入可视区域
    if (position.dy < MediaQuery.of(context).size.height && position.dy > -widget.parentScrollController.position.viewportDimension) {
      if (physics is NeverScrollableScrollPhysics) {
        setState(() {
          physics = const ScrollPhysics();
        });
        // 向下滚动进入子列表,子列表定位到底部
        if (widget.parentScrollController.position.userScrollDirection == ScrollDirection.reverse) {
          _listViewScrollController.jumpTo(_listViewScrollController.position.maxScrollExtent);
        }
        // 向上滚动进入子列表,子列表定位到顶部
        else if (widget.parentScrollController.position.userScrollDirection == ScrollDirection.forward) {
          _listViewScrollController.jumpTo(_listViewScrollController.position.minScrollExtent);
        }
      }
    }
  }

  @override
  void initState() {
    super.initState();
    _listViewScrollController = ScrollController();
    _listViewScrollController.addListener(listViewScrollListener);
    widget.parentScrollController.addListener(mainScrollListener);
  }

  @override
  void dispose() {
    _listViewScrollController.removeListener(listViewScrollListener);
    _listViewScrollController.dispose();
    widget.parentScrollController.removeListener(mainScrollListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _listViewScrollController,
      physics: physics,
      shrinkWrap: true,
      itemCount: 50,
      itemBuilder: (context, index) {
        return ListTile(
          title: Row(
            children: <Widget>[
              Text("text $index"),
            ],
          ),
        );
      },
    );
  }
}

class OtherElement extends StatelessWidget {
  final String text;

  const OtherElement({super.key, required this.text});

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      child: Center(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 40.0),
            child: Text(text, style: const TextStyle(fontSize: 30)),
          )),
    );
  }
}

优化点说明

  • 补充了子列表滚动到顶部边界的判断逻辑,滑动到顶部时自动禁用子列表滚动,将滚动权交给父列表
  • 新增了父列表滚动位置的可视区域判断,父列表反向滚动回到子列表区域时自动恢复子列表的滚动权限,同时根据滚动方向自动对齐子列表的滚动位置
  • 将父滚动控制器的监听逻辑从build方法移到initState中,在dispose中统一移除所有监听器,避免重复触发回调和内存泄漏
  • 移除了硬编码的滚动偏移判断,适配不同高度的父/子列表场景
  • 替换动效为jumpTo避免衔接时的滞后感,滚动手感和原生系统保持一致

内容的提问来源于stack exchange,提问作者Jamshed Alam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 10:45:03