如何将ListView放入PageView并垂直滚动它们?

Tay*_*yan 1 listview scroll flutter flutter-pageview

正如标题所说,我们想在垂直的 PageView 中放置一个垂直的 ListView 并让它们平滑滚动,

我们将实现这样的目标:

在此处输入图片说明

Tay*_*yan 5

这个概念:

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

为了实现这一点,我们将根据用户的触摸手势手动处理两个小部件的滚动。

编码:

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

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;
Run Code Online (Sandbox Code Playgroud)

然后初始化并处理它们:

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

  pageController = PageController();

  atTheTop = true;
  atTheBottom = false;
}

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

  super.dispose();
}
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

页面预览:

@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.
      ),
      ...
    ],
  );
}
Run Code Online (Sandbox Code Playgroud)

列表显示:

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,
          );
        },
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

typedef 注射方法:

typedef ValuesChanged<T, E> = void Function(T value, E valueTwo);
Run Code Online (Sandbox Code Playgroud)

笔记:

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

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

    更新:ScrollController为每个创建ListView并将其注入到handleDragStart&handleDragUpdate然后您将不会再次遇到该异常。

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

参考:

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