👻

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

2020/12/17に公開

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,
     this.onSizeChanged,
  })  : assert(onSizeChanged != null),
        super(key: key, child: child);

  final SizeChangedCallback onSizeChanged;

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

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

  final SizeChangedCallback onSizeChanged;

  Size _oldSize;

  
  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 {
  
  HomePageState createState() => HomePageState();
}

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

  
  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のサイズを取得できています。

<img width="505" alt="スクリーンショット 2019-07-30 15.05.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21341/0ec78fe1-415f-50d4-64e1-d0190b03f8a8.png">

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


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

この記事はQiitaの記事をエクスポートしたものです

Discussion