Wrap item different height in vertical PageView - Flutter

512

Solution 1

Use a vertical page builder and the content within a stack. This way you can take advantage of the scroll animation while having your own layout.

enter image description here

Here is the plugin I wrote. Feel free to use it and turn it into a pub.dev plugin.

Usage:

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 class:


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

Solution 2

first of all, you should use SafeArea in order to prevent your widgets go through the notch. see [this][1]. Then you should use ListView instead of PageView because PageView creates pages with the same sizes. in ListView create an array of int that stores height of widget and use it to create widgets with different size.

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://stackoverflow.com/questions/49227667/using-safearea-in-flutter#:~:text=SafeArea%20is%20basically%20a%20glorified,%22creative%22%20features%20by%20manufactures.
Share:
512
nicover
Author by

nicover

Updated on December 31, 2022

Comments

  • nicover
    nicover over 1 year

    I have a vertical PageView with different item height :

    enter image description here

    But I would like to wrap each item according their height.

    The final result I want here:

    enter image description here

    How can we do this ?