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

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

正如标题所说,我们想将垂直的 ListView 放在垂直的 PageView 中并使它们滚动 顺利,

我们将实现这样的目标:

概念:

当用户滚动列表时,如果他们到达底部并再次向同一方向滚动,我们希望页面滚动到下一个而不是列表。反之亦然。

为此,我们将根据用户的触摸手势手动处理两个小部件的滚动。

代码:

首先,在父控件的状态下,声明这些字段。

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;

然后初始化并销毁它们:

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

  pageController = PageController();

  atTheTop = true;
  atTheBottom = false;
}

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

  super.dispose();
}

现在让我们创建五个方法来处理用户的垂直拖动。

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;
}

最后,让我们构建小部件:

浏览量:

@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.
      ),
      ...
    ],
  );
}

列表视图:

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 用于注入方法:

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

备注:

  • 注意ListView中PageStorageKey的使用,这样当用户回滚到上一页时,我们可以保存列表的滚动位置

  • 如果PageView的每一页都包含一个ListView,会抛出异常ScrollController attached to multiple scroll views。它不是那么致命,您可以忽略它,一切都会正常进行。要么 如果您有解决方案,我很乐意编辑答案。

    更新: 为每个 ListView 创建 ScrollController 并注入它 to to to handleDragStart & handleDragUpdate 那你就不会遇到了 又是那个例外。

    我已经更新了上面的代码。

参考文献:

如果你有什么要说的,我在这里回复。 谢谢。