在垂直PageView中包装不同高度的项目 - Flutter

Posted

技术标签:

【中文标题】在垂直PageView中包装不同高度的项目 - Flutter【英文标题】:Wrap item different height in vertical PageView - Flutter 【发布时间】:2021-10-28 00:25:16 【问题描述】:

我有一个垂直的PageView 具有不同的项目高度:

但我想根据每个项目的高度包装每个项目。

这里我想要的最终结果:

我们怎样才能做到这一点?

【问题讨论】:

分享代码!! 抱歉,没有必要分享一个带有颜色容器的简单 PageView.builder 【参考方案1】:

首先,您应该使用SafeArea 以防止您的小部件穿过缺口。见[这个][1]。 那么您应该使用ListView 而不是PageView,因为PageView 创建的页面大小相同。在ListView 创建一个int 数组,用于存储小部件的高度并使用它来创建不同大小的小部件。

List<int> heights = [100, 120, 10];// and so on

\\then use it as follow:
ListView.builder(
            itemCount: 6,
              itemBuilder: (context, i)
                return Container(
                     height:heights[i],
                     width: 200, // or any value you want
                     padding: const EdgeInsets.all(8.0),
                     alignment: Alignment.center,
                     child: YourWidget);
              ,
          ),


  [1]: https://***.com/questions/49227667/using-safearea-in-flutter#:~:text=SafeArea%20is%20basically%20a%20glorified,%22creative%22%20features%20by%20manufactures.

【讨论】:

同意。而且我认为他也想要快照效果(将中间项目自动居中)。如果是这样,为这个列表控制器实现快照逻辑应该没问题。 感谢您的回答。是的,我正在寻找快照效果,没有简单的列表。请提供与PageView相同效果的示例,我会接受它 我认为快照效果只适用于水平滚动和相同大小的卡片。见***.com/questions/51607440/…。顺便说一句,如果您想使项目居中,请检查更新的答案【参考方案2】:

PageView 不支持动态垂直子代。使用 ListView 和 Snap 机制。

添加this 库以启用捕捉:

flutter pub add snap_scroll_physics

想法是为ListView的每个元素添加一个Snap-point:

ListView(
  physics: SnapScrollPhysics(snaps: [
        Snap(200, 50),
  ]),
...

完整的工作代码:

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

class DynamicVerticalPageView extends StatefulWidget 
  const DynamicVerticalPageView(Key? key) : super(key: key);

  @override
  State<DynamicVerticalPageView> createState() =>
      _DynamicVerticalPageViewState();


class _DynamicVerticalPageViewState extends State<DynamicVerticalPageView> 
  late double screenHeight;

  final double cardPadding = 10.0;

  late ScrollController _scrollController;

  List<double> testHeightList = [170, 300, 400, 500, 300, 100, 400, 1000];

  List<double> elementPixelPosition =
      []; //stores the pixel position of elements

  @override
  void initState() 
    super.initState();
    _scrollController = ScrollController(
        initialScrollOffset: (testHeightList[0] / 2) + cardPadding);
  

  List<Snap> getSnapList() 
    List<Snap> returnList = [];
    double lastHeight = 0;
    for (double height in testHeightList) 
      double totalHeightOfCard = height + cardPadding * 2;
      // If the scroll offset is expected to stop on the card it will snap to the middle
      returnList.add(Snap(lastHeight + totalHeightOfCard / 2,
          distance: totalHeightOfCard / 2));

      elementPixelPosition.add(lastHeight + totalHeightOfCard / 2);

      lastHeight += totalHeightOfCard;
    
    return returnList;
  

  @override
  Widget build(BuildContext context) 
    screenHeight = MediaQuery.of(context).size.height;
    return Stack(
      children: [
        NotificationListener(
          onNotification: (t) 
            if (t is ScrollEndNotification) 
              print(
                  "At Card: $elementPixelPosition.indexWhere((element) => element == _scrollController.position.pixels)");
            
            return false;
          ,
          child: ListView(
            padding: EdgeInsets.zero,
            controller: _scrollController,
            physics: SnapScrollPhysics(snaps: getSnapList()),
            //here all the snaps will be applied
            children: [
              Container(
                // container to start in the middle
                height: screenHeight / 2,
              ),
              for (int i = 0; i < testHeightList.length; i++)
                Padding(
                  padding: EdgeInsets.all(cardPadding), // padding for card
                  child: Container(
                    height: testHeightList[i], //height of the card
                    alignment: Alignment.center,
                    child: Card(
                      margin: EdgeInsets.zero,
                      elevation: 6,
                      shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(20)),
                      child: Center(
                        child: Text(
                          "Card $i",
                        ),
                      ),
                    ),
                  ),
                ),
            ],
          ),
        ),

        //a red line, just so you know where the middle is
        Center(
          child: Container(
            color: Colors.red,
            height: 5,
            width: MediaQuery.of(context).size.width,
          ),
        ),
      ],
    );
  


随意更改捕捉机制,如你所愿(例如使用更平滑的过渡或捕捉点 -> 例如使用额外的Snap.avoidZone

【讨论】:

感谢插件。现在确实存在快照效果,但用户仍然能够非常快速地滚动并快速滚动,而不是 PageView 控制每个滑动手势,无论用户快速滚动 没错,我目前也在努力解决这个问题(我正在尝试使用Snap.avoidZone)。如果您有解决方案,请告诉我。【参考方案3】:

使用垂直页面构建器和堆栈中的内容。这样您就可以在拥有自己的布局的同时利用滚动动画。

这是我写的插件。随意使用它并将其变成一个 pub.dev 插件。

用法:

import 'package:flutter/material.dart';

import 'VerticalPageViewer.dart';

class StackTest extends StatefulWidget 
  const StackTest(Key? key) : super(key: key);

  @override
  State<StackTest> createState() => _StackTestState();


class _StackTestState extends State<StackTest> 
  final List<Widget> images = [
    Container(
      decoration: BoxDecoration(
        color: Colors.red,
        borderRadius: BorderRadius.all(Radius.circular(10)),
      ),
    ),
    Container(
      decoration: BoxDecoration(
        color: Colors.cyan,
        borderRadius: BorderRadius.all(Radius.circular(10)),
      ),
    ),
    Container(
      decoration: BoxDecoration(
        color: Colors.grey,
        borderRadius: BorderRadius.all(Radius.circular(10)),
      ),
    ),
  ];
  List<double> testHeight = [100, 300, 500];

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Dynamic Height PageView',
          style: TextStyle(color: Colors.white),
        ),
        centerTitle: true,
      ),
      body: SafeArea(
        child: Container(
          child: DynamicHeightPageView(
            heightList: testHeight,
            children: images,
            onSelectedItem: (index) 
              print("index: $index");
            ,
          ),
        ),
      ),
    );
  


DynamicHeightPageView 类:


import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:collection/collection.dart';

typedef PageChangedCallback = void Function(double? page);
typedef PageSelectedCallback = void Function(int index);

class DynamicHeightPageView extends StatefulWidget 
  final List<double> heightList;
  final List<Widget> children;
  final double cardWidth;
  final ScrollPhysics? physics;
  final PageChangedCallback? onPageChanged;
  final PageSelectedCallback? onSelectedItem;
  final int initialPage;

  DynamicHeightPageView(
    required this.heightList,
    required this.children,
    this.physics,
    this.cardWidth = 300,
    this.onPageChanged,
    this.initialPage = 0,
    this.onSelectedItem,
  ) : assert(heightList.length == children.length);

  @override
  _DynamicHeightPageViewState createState() => _DynamicHeightPageViewState();


class _DynamicHeightPageViewState extends State<DynamicHeightPageView> 
  double? currentPosition;
  PageController? controller;

  @override
  void initState() 
    super.initState();
    currentPosition = widget.initialPage.toDouble();
    controller = PageController(initialPage: widget.initialPage);

    controller!.addListener(() 
      setState(() 
        currentPosition = controller!.page;

        if (widget.onPageChanged != null) 
          Future(() => widget.onPageChanged!(currentPosition));
        

        if (widget.onSelectedItem != null && (currentPosition! % 1) == 0) 
          Future(() => widget.onSelectedItem!(currentPosition!.toInt()));
        
      );
    );
  

  @override
  Widget build(BuildContext context) 
    return LayoutBuilder(builder: (context, constraints) 
      return GestureDetector(
        onTap: () 
          print("Current Element index tab: $currentPosition!.round()");
        ,
        child: Stack(
          children: [
            CardController(
              cardWidth: widget.cardWidth,
              heightList: widget.heightList,
              children: widget.children,
              currentPosition: currentPosition,
              cardViewPagerHeight: constraints.maxHeight,
              cardViewPagerWidth: constraints.maxWidth,
            ),
            Positioned.fill(
              child: PageView.builder(
                physics: widget.physics,
                scrollDirection: Axis.vertical,
                itemCount: widget.children.length,
                controller: controller,
                itemBuilder: (context, index) 
                  return Container();
                ,
              ),
            )
          ],
        ),
      );
    );
  


class CardController extends StatelessWidget 
  final double? currentPosition;
  final List<double> heightList;
  final double cardWidth;
  final double cardViewPagerHeight;
  final double? cardViewPagerWidth;
  final List<Widget>? children;

  CardController(
    this.children,
    this.cardViewPagerWidth,
    required this.cardWidth,
    required this.cardViewPagerHeight,
    required this.heightList,
    this.currentPosition,
  );

  @override
  Widget build(BuildContext context) 
    List<Widget> cardList = [];

    for (int i = 0; i < children!.length; i++) 
      var cardHeight = heightList[i];

      var cardTop = getTop(cardHeight, cardViewPagerHeight, i, heightList);
      var cardLeft = (cardViewPagerWidth! / 2) - (cardWidth / 2);

      Widget card = Positioned(
        top: cardTop,
        left: cardLeft,
        child: Container(
          width: cardWidth,
          height: cardHeight,
          child: children![i],
        ),
      );

      cardList.add(card);
    

    return Stack(
      children: cardList,
    );
  

  double getTop(
      double cardHeight, double viewHeight, int i, List<double> heightList) 
    double diff = (currentPosition! - i);
    double diffAbs = diff.abs();

    double basePosition = (viewHeight / 2) - (cardHeight / 2);

    if (diffAbs == 0) 
      //element in focus
      return basePosition;
    

    int intCurrentPosition = currentPosition!.toInt();
    double doubleCurrentPosition = currentPosition! - intCurrentPosition;

    //calculate distance between to-pull elements
    late double pullHeight;
    if (heightList.length > intCurrentPosition + 1) 
      //check for end of list
      pullHeight = heightList[intCurrentPosition] / 2 +
          heightList[intCurrentPosition + 1] / 2;
     else 
      pullHeight = heightList[intCurrentPosition] / 2;
    

    if (diff >= 0) 
      //before focus element
      double afterListSum = heightList.getRange(i, intCurrentPosition + 1).sum;

      return (viewHeight / 2) -
          afterListSum +
          heightList[intCurrentPosition] / 2 -
          pullHeight * doubleCurrentPosition;
     else 
      //after focus element
      var beforeListSum = heightList.getRange(intCurrentPosition, i).sum;
      return (viewHeight / 2) +
          beforeListSum -
          heightList[intCurrentPosition] / 2 -
          pullHeight * doubleCurrentPosition;
    
  


【讨论】:

谢谢你 使用此解决方案,“无限”或大型子列表(带有图像和视频)肯定会产生 OOM。你会根据他们的位置清除/销毁小部件吗? 我还没有看过它,但我不会将所有图像/视频都放在列表中。我宁愿制作一个 URL 列表,然后在滚动时只流式传输一定数量(例如,在焦点元素之前和之后 10 个 - 所以列表永远不会超过 21 个小部件)。这有帮助吗? 它有帮助。感谢回复

以上是关于在垂直PageView中包装不同高度的项目 - Flutter的主要内容,如果未能解决你的问题,请参考以下文章

在表格内时如何在 IE11 中包装弹性项目?

如何在 Flutter 中创建垂直滚动的 PageView?

Flutter自定义Widget—随滑动改变高度的PageView

如何在 React Native 中包装 Flatlist 项目

如何在 AppBar 中包装子小部件以获得更大的子高度和 Flutter 中的填充

Owl Carousel 在高度不相等时垂直对齐每个项目