Skip to content

Instantly share code, notes, and snippets.

@andrzejchm
Last active February 8, 2024 10:53
Show Gist options
  • Save andrzejchm/02c1728b6f31a69fde2fb4e10b636060 to your computer and use it in GitHub Desktop.
Save andrzejchm/02c1728b6f31a69fde2fb4e10b636060 to your computer and use it in GitHub Desktop.
ExpandablePageView is a PageView that will adapt its height to the currently displayed child. with animation!
import 'package:flutter/material.dart';
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,
),
);
}
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: child,
),
),
),
)
.values
.toList();
}
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() {
final size = context?.size;
if (_oldSize != size) {
_oldSize = size;
widget.onSizeChange(size);
}
}
}
@andrzejchm
Copy link
Author

Small showcase
expandable-page-view

@mgarciareimers
Copy link

Thank you so much for this Widget 👍

@MarceloFreitasx
Copy link

MarceloFreitasx commented Feb 3, 2021

Thanks for the widget, I made some changes to adapt to my need, just added an external PageController and an onPageChanged function

import 'package:flutter/material.dart';

class ExpandablePageView extends StatefulWidget {
  final List<Widget> children;
  final PageController controller;
  final ValueChanged<int> onPageChanged;

  const ExpandablePageView({
    Key key,
    @required this.children,
    this.controller,
    this.onPageChanged,
  }) : 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 = widget.controller ?? PageController() //
      ..addListener(() {
        final _newPage = _pageController.page.round();
        if (_currentPage != _newPage) {
          widget.onPageChanged(_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,
      ),
    );
  }

  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: child,
            ),
          ),
        ),
      )
      .values
      .toList();
}

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() {
    final size = context?.size;
    if (_oldSize != size) {
      _oldSize = size;
      widget.onSizeChange(size);
    }
  }
}

@Limbou
Copy link

Limbou commented Feb 11, 2021

If anyone needs a dynamic version of that page view (with itemBuilder) here is one:


class ExpandablePageView extends StatefulWidget {
  final int itemCount;
  final Widget Function(BuildContext, int) itemBuilder;
  final PageController controller;
  final ValueChanged<int> onPageChanged;
  final bool reverse;

  const ExpandablePageView({
    @required this.itemCount,
    @required this.itemBuilder,
    this.controller,
    this.onPageChanged,
    this.reverse = false,
    Key key,
  })  : assert(itemCount != null),
        assert(itemBuilder != null),
        super(key: key);

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

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

  double get _currentHeight => _heights[_currentPage];

  @override
  void initState() {
    super.initState();
    _heights = List.filled(widget.itemCount, 0, growable: true);
    _pageController = widget.controller ?? PageController();
    _pageController.addListener(_updatePage);
  }

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

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      curve: Curves.easeInOutCubic,
      tween: Tween<double>(begin: _heights.first, end: _currentHeight),
      duration: const Duration(milliseconds: 100),
      builder: (context, value, child) => SizedBox(height: value, child: child),
      child: PageView.builder(
        controller: _pageController,
        itemCount: widget.itemCount,
        itemBuilder: _itemBuilder,
        onPageChanged: widget.onPageChanged,
        reverse: widget.reverse,
      ),
    );
  }

  Widget _itemBuilder(BuildContext context, int index) {
    final item = widget.itemBuilder(context, index);
    return OverflowBox(
      minHeight: 0,
      maxHeight: double.infinity,
      alignment: Alignment.topCenter,
      child: SizeReportingWidget(
        onSizeChange: (size) => setState(() => _heights[index] = size?.height ?? 0),
        child: item,
      ),
    );
  }
  void _updatePage() {
    final newPage = _pageController.page.round();
    if (_currentPage != newPage) {
      setState(() {
        _currentPage = newPage;
      });
    }
  }
}

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

  const SizeReportingWidget({
    @required this.child,
    @required this.onSizeChange,
    Key key,
  })  : assert(child != null),
        assert(onSizeChange != null),
        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() {
    final size = context?.size;
    if (_oldSize != size) {
      _oldSize = size;
      widget.onSizeChange(size);
    }
  }
}

@proninyaroslav
Copy link

proninyaroslav commented Feb 18, 2021

@andrzejchm
It would be good to pass the key of the ExpandablePageView widget to the PageView, to be able to persist the state of the PageView itself using a PageStorageKey, for example.

@proninyaroslav
Copy link

@andrzejchm
Fixed version of SizeReportingWidget (credit: https://github.com/fluttercommunity/backdrop/blob/d4e10a5547a192052731992285bc9b2625f277b0/lib/scaffold.dart#L608). The previous one didn't work with dynamic widgets like ExpandableListTile. After expanding, the PageView height didn't resize.

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> {
  final _widgetKey = GlobalKey();
  Size _oldSize;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((_) => _notifySize());
    return NotificationListener<SizeChangedLayoutNotification>(
      onNotification: (_) {
        WidgetsBinding.instance.addPostFrameCallback((_) => _notifySize());
        return true;
      },
      child: SizeChangedLayoutNotifier(
        child: Container(
          key: _widgetKey,
          child: widget.child,
        ),
      ),
    );
  }

  void _notifySize() {
    final context = _widgetKey.currentContext;
    if (context == null) return;
    final size = context?.size;
    if (_oldSize != size) {
      _oldSize = size;
      widget.onSizeChange(size);
    }
  }
}

An example that reproduces the problem:

 ExpandablePageView(
  children: [
    ExpansionTile(
      title: Text('Title'),
      children: [
        Text('Content'),
      ],
    ),
  ],
);

@Limbou
Copy link

Limbou commented Feb 18, 2021

By permission from @andrzejchm I created a package from that code: expandable_page_view

@proninyaroslav I would be really grateful if You could create an issue or even open a pull request with suggested change for that package.

I thought that people would be more interested in just including the package name in pubspec.yaml, rather than copy and paste the code inside their projects. I also hope it will be improved in the future and found bugs will be fixed (as the one You found).

@zatovagul
Copy link

Which widget to use to put that ExpandablePageView inside, cause if I don't put height to parent container, Height of that PageView becomes 0

@jeremylcarter
Copy link

By permission from @andrzejchm I created a package from that code: expandable_page_view

@proninyaroslav I would be really grateful if You could create an issue or even open a pull request with suggested change for that package.

I thought that people would be more interested in just including the package name in pubspec.yaml, rather than copy and paste the code inside their projects. I also hope it will be improved in the future and found bugs will be fixed (as the one You found).

Thanks for the library. It is working a treat!

@fitterfly-bimal
Copy link

@jeremylcarter Thanks for important package. I am facing issue when i change page it will refresh whole parent widget. So to avoid that is there any way.

@andrzejchm
Copy link
Author

@jeremylcarter Thanks for important package. I am facing issue when i change page it will refresh whole parent widget. So to avoid that is there any way.

@fitterfly-bimal
this gist is not about the library, for any library-related questions please reach out directly at https://github.com/Limbou/expandable_page_view

@niihelium
Copy link

Big thanks to @andrzejchm and @Limbou . You are awesome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment