Flutter. PageView 中的嵌套滚动

4

我有一些简单的页面,其中包含PageView小部件,在其中还有一个ListView。但是在滚动时,PageView无法工作。原因很简单,因为指针事件被嵌套的子元素所消耗。

  @override
  Widget build(BuildContext context) {
    setupController();

    return PageView(
      controller: controllerPage,
      scrollDirection: Axis.vertical,
      children: <Widget>[
          ListView.builder(
            controller: controller,
            padding: EdgeInsets.all(AppDimens.bounds),
            itemCount: 15,
            itemBuilder: (context, index){
              return Container(
                height: 100,
                color: index %2 == 0 
                    ? Colors.amber : Colors.blueAccent,
              );
            },
          ),
        Container(color: Colors.green),
        Container(color: Colors.blue),
      ],
    );
  }

我的问题是有没有一个明智的方法让它们一起工作? 你可能会看到 PageView 的纵轴,但是使用纵向轴的 PageView 和横向 ListView 也会出现完全相同的问题。

我迄今为止尝试了什么? 我有一些解决方法。 即使它不复杂,但感觉不太好用和笨重。 通过使用 AbsorbPointer 和自定义滚动控制器。

  final controller = ScrollController();
  final controllerPage = PageController(keepPage: true);
  bool hasNestedScroll = true;

  void setupController() {
    controller.addListener(() {

      if (controller.offset + 5 > controller.position.maxScrollExtent &&
          !controller.position.outOfRange) {
        /// Swap to Inactive, if it was not
        if (hasNestedScroll) {
          setState(() {
            hasNestedScroll = false;
          });
        }
      } else {
        /// Swap to Active, if it was not
        if (!hasNestedScroll) {
          setState(() {
            hasNestedScroll = true;
          });
        }
      }
    });

    controllerPage.addListener(() {
      if (controllerPage.page == 0) {
        setState(() {
          hasNestedScroll = true;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    setupController();

    return PageView(
      controller: controllerPage,
      scrollDirection: Axis.vertical,
      children: <Widget>[
        AbsorbPointer(
          absorbing: !hasNestedScroll,
          child: ListView.builder(
            controller: controller,
            padding: EdgeInsets.all(AppDimens.bounds),
            itemCount: 15,
            itemBuilder: (context, index){
              return Container(
                height: 100,
                color: index %2 == 0
                    ? Colors.amber : Colors.blueAccent,
              );
            },
          ),
        ),
        Container(color: Colors.green),
        Container(color: Colors.blue),
      ],
    );
  }

你是想同时在PageView和ListView上滚动吗? - Alaric James Hartsock
@AlaricJamesHartsock 不,我正在尝试滚动ListView,只要它有空间。一旦ListView的滚动到达末尾,我需要滚动PageView - GensaGames
@AlaricJamesHartsock 你有什么建议吗? - GensaGames
抱歉,我累垮了。也许可以尝试查看API,并查看是否可以手动将滚动控制从列表视图移交给页面视图,一旦它完全滚动到底部。希望你能找到你要找的东西。 - Alaric James Hartsock
2个回答

5

最近我也遇到了类似的情况,在研究和阅读有关Flutter手势如何在内部工作的文章后,我找到了一个足够好的解决方案。

所以我使用了RawGestureDetector,这提供了手势的低级Widget处理,并通过将它们的physics设置为NeverScrollableScrollPhysics来禁用了PageView和ListView的滚动。这意味着要使用它们各自的控制器PageViewControllerScrollController来滚动这些小部件。现在根据ListView的位置和状态,选择两个控制器中的活动控制器并使用该控制器进行拖动。主类应该是这样的:

void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: Scaffold(appBar: AppBar(), body: NestedContainerWidget()));
  }
}

class NestedContainerWidget extends StatefulWidget {
  @override
  _NestedContainerWidgetState createState() => _NestedContainerWidgetState();
}

class _NestedContainerWidgetState extends State<NestedContainerWidget> {
  PageController _pageController;
  ScrollController _listScrollController;
  ScrollController _activeScrollController;
  Drag _drag;

  @override
  void initState() {
    super.initState();
    _pageController = PageController();
    _listScrollController = ScrollController();
  }

  @override
  void dispose() {
    _pageController.dispose();
    _listScrollController.dispose();
    super.dispose();
  }

  void _handleDragStart(DragStartDetails details) {
    if (_listScrollController.hasClients &&
        _listScrollController.position.context.storageContext != null) {
      final RenderBox renderBox =
          _listScrollController.position.context.storageContext.findRenderObject();
      if (renderBox.paintBounds
          .shift(renderBox.localToGlobal(Offset.zero))
          .contains(details.globalPosition)) {
        _activeScrollController = _listScrollController;
        _drag = _activeScrollController.position.drag(details, _disposeDrag);
        return;
      }
    }
    _activeScrollController = _pageController;
    _drag = _pageController.position.drag(details, _disposeDrag);
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    if (_activeScrollController == _listScrollController &&
        details.primaryDelta < 0 &&
        _activeScrollController.position.pixels ==
            _activeScrollController.position.maxScrollExtent) {
      _activeScrollController = _pageController;
      _drag?.cancel();
      _drag = _pageController.position.drag(
          DragStartDetails(
              globalPosition: details.globalPosition, localPosition: details.localPosition),
          _disposeDrag);
    }
    _drag?.update(details);
  }

  void _handleDragEnd(DragEndDetails details) {
    _drag?.end(details);
  }

  void _handleDragCancel() {
    _drag?.cancel();
  }

  void _disposeDrag() {
    _drag = null;
  }

  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: <Type, GestureRecognizerFactory>{
        VerticalDragGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
                () => VerticalDragGestureRecognizer(), (VerticalDragGestureRecognizer instance) {
          instance
            ..onStart = _handleDragStart
            ..onUpdate = _handleDragUpdate
            ..onEnd = _handleDragEnd
            ..onCancel = _handleDragCancel;
        })
      },
      behavior: HitTestBehavior.opaque,
      child: PageView(
        controller: _pageController,
        scrollDirection: Axis.vertical,
        physics: const NeverScrollableScrollPhysics(),
        children: [
          Center(child: Text('Page 1')),
          ListView(
            controller: _listScrollController,
            physics: const NeverScrollableScrollPhysics(),
            children: List.generate(
              20,
              (int index) {
                return ListTile(title: Text('Item $index'));
              },
            ),
          ),
        ],
      ),
    );
  }
}

看一下_handleDragStart_handleDragUpdate中的逻辑,根据ListView小部件和它的滚动状态确定应该滚动哪个控制器。

我写了一篇详细的文章解决这个问题,你可以在这里查看 - https://medium.com/@Mak95/nested-scrolling-listview-inside-pageview-in-flutter-a57b7a6241b1

这个解决方案可以改进,所以输入将非常受欢迎。


始终为您的问题添加标签,以便正确的人群可以查看它。 - Navneet kumar
有没有办法在页面视图中拥有多个嵌套的列表视图子项? - austin

1

我使用了this answer来帮助我解决相同的问题。

基本上,你可以使用NotificationListener监听可滚动子元素的OverscrollNotification事件,并通过父元素的控制器来滚动它。

代码:

final pageCntrler = PageController();

_scrollDown() async {
  await pageCntrler.nextPage(
    duration: scrollDuration,
    curve: scrollCurve,
  );
}

_scrollUp() async {
  await pageCntrler.previousPage(
    duration: scrollDuration,
    curve: scrollCurve,
  );
}

@override
Widget build(BuildContext context) {
  return PageView(
    controller: pageCntrler,
    scrollDirection: Axis.vertical,
    physics: NeverScrollableScrollPhysics(),
    children: [
      NotificationListener(
        onNotification: (notification) {
          if (notification is OverscrollNotification) {
            if (notification.overscroll > 0) {
              _scrollDown();
            } else {
              _scrollUp();
            }
          }
        },
        child: SingleChildScrollView(),
      )
    ],
  );
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接