1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PageViewの高さを表示内容に動的に合わせる方法

Posted at

はじめに

Flutter開発でページの内容に応じてPageViewのサイズを動的に変更する方法がないか調べたので記事にします。
例えば、Image.networkを使って画像を表示し、表示した後画像に合わせてPageViewの大きさを変更するようなケースです。

ソースコード

class FlexiblePageView extends StatefulWidget {
  final List<Widget> children;
  final double initHeight;
  final Function(double)? onHeightChanged;

  const FlexiblePageView({
    Key? key,
    required this.children,
    this.initHeight = 100,
    this.onHeightChanged,
  }) : super(key: key);

  @override
  State<FlexiblePageView> createState() => _FlexiblePageViewState();
}

class _FlexiblePageViewState extends State<FlexiblePageView> {
  final GlobalKey viewKey = GlobalKey();
  late PageController _pageController;

  /// PageViewの高さ
  late double height;

  /// 各ページの高さのリスト
  ///
  /// ページを切り替えた時に高さを変更するように、各ぺーじの高さを保持しておく
  List<double> pageHeightList = [];

  @override
  void initState() {
    _pageController = PageController();
    height = widget.initHeight;
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      height: height,
      child: PageView.builder(
        controller: _pageController,
        itemCount: widget.children.length,
        onPageChanged: (value) {
          if (pageHeightList.length <= value) {
            return;
          }

          // 保存しているページの高さがあれば設定する
          setState(() {
            height = pageHeightList[value];
          });
        },
        itemBuilder: (context, index) {
          return OverflowBox(
            maxHeight: double.infinity,
            child: SizeReportingWidget(
              child: widget.children[index],
              onSizeChange: (size) {
                // 変更通知された高さと初期値の高さが同じ場合、
                // pageHeightListに高さを追加できないケースがあるので、
                // height == widget.initHeight場合は、追加する
                if (height == widget.initHeight || size.height != height) {
                  setState(
                    () {
                      height = size.height;
                      if (pageHeightList.length <= index) {
                        pageHeightList.add(size.height);
                      } else {
                        pageHeightList[index] = size.height;
                      }
                    },
                  );
                }
              },
            ),
          );
        },
      ),
    );
  }
}

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
  State<SizeReportingWidget> 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!);
    }
  }
}

使う側

FlexiblePageView(
  children : [
    const SizedBox(
      height: 100,
      child: Center(
        child: Text(
          "height: 100",
          style: TextStyle(fontSize: 20),
        ),
      ),
    ),
    const SizedBox(
      height: 200,
      child: Center(
        child: Text(
          "height: 200",
          style: TextStyle(fontSize: 20),
        ),
      ),
    ),
    const SizedBox(
      height: 300,
      child: Center(
        child: Text(
          "height: 300",
          style: TextStyle(fontSize: 20),
        ),
      ),
    )
  ],
),

動的にサイズを変更する仕組み

表示しているWidgetに合わせて、動的にPageViewの大きさを変更するには、以下の手順が必要です。

  • PageView.builderのitemBuilder内でWidgetの描画完了後、高さを取得する
  • 取得した高さでPageViewの大きさを変更する

取得した高さでPageViewの大きさを変更するのは、StatefulWidgetのsetStateで変更できますが、
描画完了後高さを取得するには、子Widget自身から描画完了後に高さを通知して、PageViewを使用しているWidgetで取得する必要があります。

Widgetの構成

今回の記事では、以下のようなWidgetの構成にして、高さを取得する部分を実現しています。

  • FlexiblePageView
    PageViewを使用している自作のWidgetです。
  • SizeReportingWidget
    使いやすいようにラッピングした自作のWidgetで、NotificationListeneSizeChangedLayoutNotifierを使い、親のWidgetにサイズを通知するようにしています。
  • NotificationListener
    子Widgetの状態変化のイベントを購読して取得することができるFlutter標準のWidgetです。詳細は後述。
  • SizeChangedLayoutNotifier
    子Widgetのサイズが変化した時に通知を発行するFlutter標準のWidgetです。詳細は後述。

シーケンス図

NotificationListener

NotificationListenerとは、子Widgetの状態変化のイベントを購読して取得することができるウィジェットです。
今回の記事では、SizeChangedLayoutNotificationを指定し、大きさの変化を購読しています。

下記の記事がわかりやすかったです。

公式

SizeChangedLayoutNotification

SizeChangedLayoutNotificationは、子Widgetのサイズが変更されたら通知を発行するWidgetです。

NotificationListenerで通知を購読し、SizeChangedLayoutNotificationで通知を発行するという仕組みとなります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?