はじめに
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で、NotificationListene
、SizeChangedLayoutNotifier
を使い、親のWidgetにサイズを通知するようにしています。 - NotificationListener
子Widgetの状態変化のイベントを購読して取得することができるFlutter標準のWidgetです。詳細は後述。 - SizeChangedLayoutNotifier
子Widgetのサイズが変化した時に通知を発行するFlutter標準のWidgetです。詳細は後述。
シーケンス図
NotificationListener
NotificationListenerとは、子Widgetの状態変化のイベントを購読して取得することができるウィジェットです。
今回の記事では、SizeChangedLayoutNotification
を指定し、大きさの変化を購読しています。
下記の記事がわかりやすかったです。
公式
SizeChangedLayoutNotification
SizeChangedLayoutNotification
は、子Widgetのサイズが変更されたら通知を発行するWidgetです。
NotificationListener
で通知を購読し、SizeChangedLayoutNotification
で通知を発行するという仕組みとなります。