Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: adjust height of PageView/Horizontal ListView based on current child

Tags:

flutter

dart

I'm trying to create a carousel with items of variable heights. When using PageView or ListView with horizontal scrolling, I need to give it a constant height, like this:

class CarouselVariableHightState extends State<CarouselVariableHight> {
  double height = 200;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Carousel')),
      body: ListView(
        children: <Widget>[
          Text('My Carousel'),
          Container(
            height: height,
            child: PageView(
              children: <Widget>[
                _buildCarouselItem(),
                _buildCarouselItem(),
                _buildCarouselItem(),
                _buildCarouselItem(),
              ],
            ),
          ),
          Text('end of List')
        ],
      ),
    );
  }

  Widget _buildCarouselItem() {
    return Column(
      children: [
        Container(
          color: Colors.red,
          height: Random().nextInt(200).toDouble(),
          child: Text('Text with random length')
        )
      ]
    );
  }
}

But my items contain text and I don't know the length of it. I don't wan't to limit the height of a CarouselItem because it might cut off the text. How could I have something like a PageView, that adjust its own size based on the currently shown item?

I thought a solution could be to get the height of the CarouselItem and then set it to the height of the PageView. However I couldn't figure out a way to get the height my current Carousel item.

Any Ideas?

like image 370
Jonas Avatar asked Feb 04 '19 19:02

Jonas


People also ask

How do I change the PageView height in flutter?

You cannot change the height of the elements when PageView is build. So if you need to have for example a server request make it before you render the carusel.

What is PageView builder in flutter?

PageView. builder creates a scrollable list that works page by page using widgets that are created on-demand. This constructor is appropriate for page views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.


2 Answers

Below you can find an example of a PageView that adapts its height to currently displayed child. With animation:

ExpandablePageView example

The idea is:

  1. We wrap each child of PageView with a custom widget that can report widget size after each frame. Here I called it SizeReportingWidget.
  2. Each height is saved in a list, called _heights on an index that corresponds to the child's index.
  3. Attach a PageController that will let you determine the currently displayed page's index. Here we save it to a field called _currentPage
  4. Wrap PageView in a SizedBox and give it the height of the currently displayed child. I've added a convenient getter called _currentHeight to retrieve the height.
  5. (Optional) Wrap the SizedBox in TweenAnimationBuilder, so that the height changes are smooth and animated rather than clanky jumps.

ExpandablePageView

class ExpandablePageView extends StatefulWidget {
  final List<Widget> children;

  const ExpandablePageView({
    Key key,
    @required this.children,
  }) : super(key: key);

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

class _ExpandablePageViewState extends State<ExpandablePageView> with TickerProviderStateMixin {
  PageController _pageController;
  List<double> _heights;
  int _currentPage = 0;

  double get _currentHeight => _heights[_currentPage];

  @override
  void initState() {
    _heights = widget.children.map((e) => 0.0).toList();
    super.initState();
    _pageController = PageController() //
      ..addListener(() {
        final _newPage = _pageController.page.round();
        if (_currentPage != _newPage) {
          setState(() => _currentPage = _newPage);
        }
      });
  }


 @override
 void dispose() {
   _pageController.dispose();
   super.dispose();
 }


  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      curve: Curves.easeInOutCubic,
      duration: const Duration(milliseconds: 100),
      tween: Tween<double>(begin: _heights[0], end: _currentHeight),
      builder: (context, value, child) => SizedBox(height: value, child: child),
      child: PageView(
        controller: _pageController,
        children: _sizeReportingChildren
            .asMap() //
            .map((index, child) => MapEntry(index, child))
            .values
            .toList(),
      ),
    );
  }

  List<Widget> get _sizeReportingChildren => widget.children
      .asMap() //
      .map(
        (index, child) => MapEntry(
          index,
          OverflowBox(
            //needed, so that parent won't impose its constraints on the children, thus skewing the measurement results.
            minHeight: 0,
            maxHeight: double.infinity,
            alignment: Alignment.topCenter,
            child: SizeReportingWidget(
              onSizeChange: (size) => setState(() => _heights[index] = size?.height ?? 0),
              child: Align(child: child),
            ),
          ),
        ),
      )
      .values
      .toList();
}

SizeReportingWidget

class SizeReportingWidget extends StatefulWidget {
  final Widget child;
  final ValueChanged<Size> onSizeChange;

  const SizeReportingWidget({
    Key key,
    @required this.child,
    @required this.onSizeChange,
  }) : super(key: key);

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

class _SizeReportingWidgetState extends State<SizeReportingWidget> {
  Size _oldSize;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((_) => _notifySize());
    return widget.child;
  }

  void _notifySize() {
    if (!this.mounted) {
      return;
    }
    final size = context?.size;
    if (_oldSize != size) {
      _oldSize = size;
      widget.onSizeChange(size);
    }
  }
}
like image 127
Andrzej Chmielewski Avatar answered Oct 19 '22 00:10

Andrzej Chmielewski


There are 2 possible solutions: right one and hacky one.

  1. The Right solution: Because PageView is trying to get all posible height, we can't use it inside of any widget, that dont' have height. So if we don't have height we need to use another widget, for example StackWidget and re-create PageView behavior: swipes, animations etc. It won't be a big library, if you want to make just simple things, less than 200-300 lines I guess.

  2. The hacky solution is to check the height of all elements before you build the PageView widget. You cannot change the height of the elements when PageView is build. So if you need to have for example a server request make it before you render the carusel.

To get the height, it easier to use IndexedStack, becouse it has the height of the highest element, but show only the first one (by default).

  • use keys to get the height of IndexedStack
  • use setState to save it.
  • use obtained height as the height of PageView.

enter image description here

class CarouselVariableHight extends StatefulWidget {
  @override
  CarouselVariableHightState createState() => CarouselVariableHightState();
}

final widgetHeights = <double>[
  Random().nextInt(300).toDouble(),
  Random().nextInt(300).toDouble(),
  Random().nextInt(300).toDouble(),
  Random().nextInt(300).toDouble(),
];

class CarouselVariableHightState extends State<CarouselVariableHight> {
  double height;
  GlobalKey stackKey = GlobalKey();
  bool widgetHasHeigh;

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
    super.initState();
  }

  _afterLayout(_) {
    final RenderBox renderBoxRed = stackKey.currentContext.findRenderObject();
    final sizeRed = renderBoxRed.size;
    setState(() {
      height = sizeRed.height;
    });
  }

  final caruselItems = widgetHeights
      .map((height) => Column(children: [
            Container(
                color: Colors.red,
                height: Random().nextInt(300).toDouble(),
                child: Text('Text with random length'))
          ]))
      .toList();

  _buildStack() {
    Widget firstElement;
    if (height == null) {
      firstElement = Container();
    } else {
      firstElement = Container(
        height: height,
        child: PageView(
          children: caruselItems,
        ),
      );
    }

    return IndexedStack(
      key: stackKey,
      children: <Widget>[
        firstElement,
        ...caruselItems,
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Carousel')),
      body: ListView(
        children: <Widget>[
          Text('My Carousel'),
          _buildStack(),
          Text('end of List'),
        ],
      ),
    );
  }
}
like image 23
Kherel Avatar answered Oct 18 '22 23:10

Kherel