简体   繁体   English

如何将ListView放在PageView中并垂直滚动?

[英]How to Put ListView Inside PageView and Scroll Both of Them Vertically?

As the title says, we wanna put a vertical ListView inside a vertical PageView and make them scroll smoothly,正如标题所说,我们想将垂直的 ListView 放在垂直的 PageView 中并使它们平滑滚动,

We will achieve something like that:我们将实现这样的目标:

在此处输入图像描述

The Concept:这个概念:

When the user scrolls the list, if they reach its bottom and scroll in the same direction again, we want the page to scroll to the next one not the list.当用户滚动列表时,如果他们到达底部并再次向同一方向滚动,我们希望页面滚动到下一个而不是列表。 And vice versa.反之亦然。

To achieve that we are gonna handle the scrolling of both widgets manually, depending on the touch gestures of the user.为了实现这一点,我们将根据用户的触摸手势手动处理两个小部件的滚动。

The Code:编码:

Firstly, in the state of the parent widget, declare these fields.首先,在父widget的state中,声明这些字段。

PageController pageController;
ScrollController activeScrollController;
Drag drag;

//These variables To detect if we are at the
//top or bottom of the list.
bool atTheTop;
bool atTheBottom;

Then initialize and dispose them:然后初始化并处理它们:

@override
void initState() {
  super.initState();

  pageController = PageController();

  atTheTop = true;
  atTheBottom = false;
}

@override
void dispose() {
  pageController.dispose();

  super.dispose();
}

now let's create five methods for handling the vertical dragging of the user.现在让我们创建五个方法来处理用户的垂直拖动。

void handleDragStart(DragStartDetails details, ScrollController 
scrollController) {
  if (scrollController.hasClients) {
    if (scrollController.position.context.storageContext != null) {
      if (scrollController.position.pixels == scrollController.position.minScrollExtent) {
        atTheTop = true;
      } else if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
        atTheBottom = true;
      } else {
        atTheTop = false;
        atTheBottom = false;

        activeScrollController = scrollController;
        drag = activeScrollController.position.drag(details, disposeDrag);
        return;
      }
    }
  }

  activeScrollController = pageController;
  drag = pageController.position.drag(details, disposeDrag);
}

void handleDragUpdate(DragUpdateDetails details, ScrollController 
scrollController) {
  if (details.delta.dy > 0 && atTheTop) {
    //Arrow direction is to the bottom.
    //Swiping up.

    activeScrollController = pageController;
    drag?.cancel();
    drag = pageController.position.drag(
        DragStartDetails(globalPosition: details.globalPosition, localPosition: details.localPosition),
        disposeDrag);
  } else if (details.delta.dy < 0 && atTheBottom) {
    //Arrow direction is to the top.
    //Swiping down.

    activeScrollController = pageController;
    drag?.cancel();
    drag = pageController.position.drag(
        DragStartDetails(
          globalPosition: details.globalPosition,
          localPosition: details.localPosition,
        ),
        disposeDrag);
  } else {
    if (atTheTop || atTheBottom) {
      activeScrollController = scrollController;
      drag?.cancel();
      drag = scrollController.position.drag(
          DragStartDetails(
            globalPosition: details.globalPosition,
            localPosition: details.localPosition,
          ),
          disposeDrag);
    }
  }
  drag?.update(details);
}

void handleDragEnd(DragEndDetails details) {
  drag?.end(details);

  if (atTheTop) {
    atTheTop = false;
  } else if (atTheBottom) {
    atTheBottom = false;
  }
}

void handleDragCancel() {
  drag?.cancel();
}

void disposeDrag() {
  drag = null;
}

And Finally, let's build the widgets:最后,让我们构建小部件:

PageView:页面预览:

@override
Widget build(BuildContext context) {
  return PageView(
    controller: pageController,
    scrollDirection: Axis.vertical,
    physics: const NeverScrollableScrollPhysics(),
    children: [
      MyListView(
        handleDragStart: handleDragStart,
        handleDragUpdate: handleDragUpdate,
        handleDragEnd: handleDragEnd,
        pageStorageKeyValue: '1', //Should be unique for each widget.
      ),
      ...
    ],
  );
}

ListView:列表显示:

class MyListView extends StatefulWidget {
  const MyListView({
    Key key,
    @required this.handleDragStart,
    @required this.handleDragUpdate,
    @required this.handleDragEnd,
    @required this.pageStorageKeyValue,
  })  : assert(handleDragStart != null),
        assert(handleDragUpdate != null),
        assert(handleDragEnd != null),
        assert(pageStorageKeyValue != null),
        super(key: key);

  final ValuesChanged<DragStartDetails, ScrollController> handleDragStart;
  final ValuesChanged<DragUpdateDetails, ScrollController> handleDragUpdate;
  final ValueChanged<DragEndDetails> handleDragEnd;
  
  //Notice here, the key to save the position scroll of the list.
  final String pageStorageKeyValue;

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

class _MyListViewState extends State<MyListView> {
  ScrollController scrollController;

  @override
  void initState() {
    super.initState();

    scrollController = ScrollController();
  }

  @override
  void dispose() {
    scrollController.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onVerticalDragStart: (details) {
        widget.handleDragStart(details, scrollController);
      },
      onVerticalDragUpdate: (details) {
        widget.handleDragUpdate(details, scrollController);
      },
      onVerticalDragEnd: widget.handleDragEnd,
      child: ListView.separated(
        key: PageStorageKey<String>(widget.pageStorageKeyValue),
        physics: const NeverScrollableScrollPhysics(),
        controller: scrollController,
        itemCount: 15,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Item $index'),
          );
        },
        separatorBuilder: (context, index) {
          return const Divider(
            thickness: 3,
          );
        },
      ),
    );
  }
}

typedef for injecting the methods: typedef用于注入方法:

typedef ValuesChanged<T, E> = void Function(T value, E valueTwo);

Notes:笔记:

  • Notice the using of PageStorageKey in the ListView, so that we can save the scroll position of the list if the user scrolls back to the previous page.注意ListView中PageStorageKey的使用,这样如果用户回滚到上一页,我们就可以保存列表的滚动position。

  • If each page of the PageView will contain a ListView, an exception will be thrown saying that ScrollController attached to multiple scroll views .如果 PageView 的每个页面都包含一个 ListView,则会抛出一个异常,说ScrollController attached to multiple scroll views It's not that fatal, you can ignore it and everything will work fine.它不是那么致命,您可以忽略它,一切都会正常工作。 Or if you have a solution, I'll gladly edit the answer.或者,如果您有解决方案,我很乐意编辑答案。

    Update: create ScrollController for each ListView and inject it to to handleDragStart & handleDragUpdate then you will not encounter that exception again.更新:为每个ListView创建ScrollController并将其注入到handleDragStarthandleDragUpdate然后您将不会再次遇到该异常。

    I've updated the code above.我已经更新了上面的代码。

References:参考:

If you have anything to say, I'm here to reply.如果你有什么要说的,我在这里回复。 Thanks.谢谢。

I've modified Tayan's response to suit cases when scrolling view are too small to fit entire screen.当滚动视图太小而无法容纳整个屏幕时,我修改了 Tayan 对诉讼案件的回应。 I've added horizontal orientation support also and moved drag handling logic to the widget itself.我还添加了水平方向支持,并将拖动处理逻辑移至小部件本身。 Also there is support for any kind of scrolling view (not only ListView) Here is the code:还支持任何类型的滚动视图(不仅是 ListView)这是代码:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

///This widget provides correct scrolling and swiping behavior when scrolling view are placed inside pageview with same direction
///The widget works both for vertical and horizontal scrolling direction
///To use this widget you have to do following:
///* set physics: NeverScrollableScrollPhysics(parent: ClampingScrollPhysics()) argument for both PageView and ScrollView
///* create scrollController for ScrollView and pageController for PageView. Do not forget to dispose then at dispose() State callback
///* make sure that scrolling direction on both views are the same and equals to scrollDirection argument here

class PageViewScrollableChild extends StatefulWidget {
  final Widget child;
  final ScrollController scrollController;
  final PageController pageController;
  final Axis scrollDirection;

  const PageViewScrollableChild(
      {Key? key,
      required this.scrollController,
      required this.pageController,
      required this.child,
      required this.scrollDirection})
      : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _PageViewScrollableChildState();
  }
}

class _PageViewScrollableChildState extends State<PageViewScrollableChild> {
  late bool atTheStart;
  late bool atTheEnd;
  ///true if scroll view content does not overscroll screen size
  late bool bothSides;

  ScrollController? activeScrollController;
  Drag? drag;

  @override
  void initState() {
    super.initState();

    atTheStart = true;
    atTheEnd = false;
    bothSides = false;
  }

  void handleDragStart(DragStartDetails details, ScrollController scrollController) {
    if (scrollController.hasClients) {
      if (scrollController.position.minScrollExtent == 0 && scrollController.position.maxScrollExtent == 0) {
        bothSides = true;
      } else if (scrollController.position.pixels <= scrollController.position.minScrollExtent) {
        atTheStart = true;
      } else if (scrollController.position.pixels >= scrollController.position.maxScrollExtent) {
        atTheEnd = true;
      } else {
        atTheStart = false;
        atTheEnd = false;

        activeScrollController = scrollController;
        drag = activeScrollController?.position.drag(details, disposeDrag);
        return;
      }
    }

    activeScrollController = widget.pageController;
    drag = widget.pageController.position.drag(details, disposeDrag);
  }

  void handleDragUpdate(DragUpdateDetails details, ScrollController scrollController) {
    final offset = widget.scrollDirection == Axis.vertical ? details.delta.dy : details.delta.dx;
    if (offset > 0 && (atTheStart || bothSides)) {
      //Arrow direction is to the bottom.
      //Swiping up.

      activeScrollController = widget.pageController;
      drag?.cancel();
      drag = widget.pageController.position.drag(
          DragStartDetails(globalPosition: details.globalPosition, localPosition: details.localPosition), disposeDrag);
    } else if (offset < 0 && (atTheEnd || bothSides)) {
      //Arrow direction is to the top.
      //Swiping down.

      activeScrollController = widget.pageController;
      drag?.cancel();
      drag = widget.pageController.position.drag(
          DragStartDetails(
            globalPosition: details.globalPosition,
            localPosition: details.localPosition,
          ),
          disposeDrag);
    } else if (atTheStart || atTheEnd) {
      activeScrollController = scrollController;
      drag?.cancel();
      drag = scrollController.position.drag(
          DragStartDetails(
            globalPosition: details.globalPosition,
            localPosition: details.localPosition,
          ),
          disposeDrag);
    }

    drag?.update(details);
  }

  void handleDragEnd(DragEndDetails details) {
    drag?.end(details);

    if (atTheStart) {
      atTheStart = false;
    } else if (atTheEnd) {
      atTheEnd = false;
    }
  }

  void handleDragCancel() {
    drag?.cancel();
  }

  void disposeDrag() {
    drag = null;
  }

  @override
  Widget build(BuildContext context) {
    final scrollDirection = widget.scrollDirection;
    return GestureDetector(
      onVerticalDragStart:
          scrollDirection == Axis.vertical ? (details) => handleDragStart(details, widget.scrollController) : null,
      onVerticalDragUpdate:
          scrollDirection == Axis.vertical ? (details) => handleDragUpdate(details, widget.scrollController) : null,
      onVerticalDragEnd: scrollDirection == Axis.vertical ? (details) => handleDragEnd(details) : null,
      onHorizontalDragStart:
          scrollDirection == Axis.horizontal ? (details) => handleDragStart(details, widget.scrollController) : null,
      onHorizontalDragUpdate:
          scrollDirection == Axis.horizontal ? (details) => handleDragUpdate(details, widget.scrollController) : null,
      onHorizontalDragEnd: scrollDirection == Axis.horizontal ? (details) => handleDragEnd(details) : null,
      child: widget.child,
    );
  }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM