Edited at

Flutterで、あるWidgetのレイアウト後の実際のサイズを取得する

FlutterでのUIビルディングは、最終的に表示されるUIの構造(とプロパティ)を、Widgetのツリーで定義します。アプリを作る開発者は、そのUIの各要素が、どのようにレイアウトされレンダリングされたかの詳細を知る必要は、ほとんどありません。

ただし、まれにではありますが、あるWidgetが、実際のレイアウト時にどのようなサイズになっているかを知りたいケースがある……かもしれません。

アプリ開発者が通常扱う Widget とは別の、より低レイヤの Render でレイアウトが行われており、ここまで踏み込むとサイズがわかります。

そこで以下に、あるWidgetのレイアウト・サイズを、コールバックとして受け取るSizeListenableContainerというクラスを実装してみました。

※コードはMITライセンスとします

import 'package:flutter/widgets.dart';

import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

typedef SizeChangedCallback = void Function(Size size);

class SizeListenableContainer extends SingleChildRenderObjectWidget {
const SizeListenableContainer({
Key key,
Widget child,
@required this.onSizeChanged,
}) : assert(onSizeChanged != null),
super(key: key, child: child);

final SizeChangedCallback onSizeChanged;

@override
_SizeListenableRenderObject createRenderObject(BuildContext context) {
return _SizeListenableRenderObject(onSizeChanged: onSizeChanged);
}
}

class _SizeListenableRenderObject extends RenderProxyBox {
_SizeListenableRenderObject({
RenderBox child,
@required this.onSizeChanged,
}) : assert(onSizeChanged != null),
super(child);

final SizeChangedCallback onSizeChanged;

Size _oldSize;

@override
void performLayout() {
super.performLayout();

final Size size = this.size;
if (size != _oldSize) {
_oldSize = size;
_callback(size);
}
}

void _callback(Size size) {
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
onSizeChanged(size);
});
}
}

これを使う側の例は以下の通り。

class HomePage extends StatefulWidget {

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

class HomePageState extends State<HomePage> {
String _text = "あ";
Size _size;

@override
Widget build(BuildContext context) {
final String sizeText = _size?.toString() ?? "null";
return Scaffold(
appBar: AppBar(
title: Text("Widget Size Listening"),
),
body: Container(
padding: const EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
SizeListenableContainer(
child: Container(
color: Colors.blue,
child: Text(
_text,
style: const TextStyle(color: Colors.white),
),
),
onSizeChanged: (Size size) {
setState(() {
_size = size;
});
},
),
Text("上のWidgetのSizeは(${sizeText})です"),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
_text += "あ";
});
},
),
);
}
}

以下のように、Textを含むContainerのサイズを取得できています。

スクリーンショット 2019-07-30 15.05.08.png

onSizeChangedでsetStateするのはよいですが、それによってSizeListenableContainer.childのサイズが変わるとリビルド・ループになるので、使い方は気をつけてください。


なお、サイズを知りたいというより、レイアウトをコントロールしたいというのが目的あれば CustomSingleChildLayoutCustomMultiChildLayout を使います。