[Flutter]InteractiveViewerでコンテンツが左上に張り付く問題の対策


## 問題点
下記を満たしている時、コンテンツが左上に張り付きます。
こいつをセンタリングさせるのが目標です。

- InteractiveViewerのサイズが中身のコンテンツのサイズを超えている
- constrained=falseが設定されている

ちなみに、~~Flutter for Webでは~~ （→Androidでも再現しました） その状態で少し拡大したらminScale/maxScaleを無視してInteractiveViewer全体にコンテンツが広がります。バグの香りがします。
→[起票してみました](https://github.com/flutter/flutter/issues/65304)

■左上に張り付く
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700353/0d065105-615e-2d76-bfac-e24288642815.png" width=400>

■理想形
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700353/6b9684d6-1a00-d179-0aa6-5d09ddcb9e86.png" width=400>


## InteractiveViewerとは

比較的最近追加された、UIScrollViewみたいなものです。
SingleChildScrollViewなどは一方向のスクロールですが、InteractiveViewerは縦横スクロールに対応しています。

以下の記事に詳しくまとめていただいている方が居ます。
 [Flutter InteractiveViewerの使い方](https://qiita.com/kurun_pan/items/779c1c2b81853ed428b7)

## 対処方針

今回対象とするコンテンツの要件は以下の通りです。

- ユーザーによる拡縮はOK、初期表示で拡大されているのはNG
- 画面幅が余った際はコンテンツをセンタリングさせて等倍で表示

InteractiveViewerにalignmentみたいなフラグがあればそれで終わる話なのですが、無さそうなので中身のコンテンツのサイズを調整して解決する事にしました。
ただし、InteractiveViewerでconstrained=falseをセットしていると、内側からはちょうどいいサイズに調整しようにも求められるサイズが取得できないため少し遠回りな処理となっています。

1. 仮のContainerを設置する
1. レンダリングが終わったらサイズを取得する
1. 本命のInteractiveViewerを設置する

流れとしてはこんな感じで処理します。あと画面サイズの変更(回転・ブラウザの拡縮)の際には再計算させています。
中身のコンテンツのサイズが分からない場合はそちらも計算してやる必要がありますが同じような形で実装できると思います。


## 対処コード

動作確認はAndroid/Webで行っています。

```dart
import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';

class GraphView extends StatefulWidget {
  @override
  _GraphViewState createState() => _GraphViewState();
}

class _GraphViewState extends State<GraphView> with WidgetsBindingObserver {
  /// WidgetsBindingのobserverとして登録済みか？
  bool _observerRegistered = false;

  /// InteractiveViewerのサイズを計測するためのContainerのkey.
  final GlobalKey _measuringContainerKey = GlobalKey();

  /// InteractiveViewerのサイズ.
  Size _renderingSize;

  @override
  Widget build(BuildContext context) {
    if (_renderingSize == null) {
      // サイズが分からない場合

      // サイズ計測用Containerのレンダリング終了時、描画サイズを記憶する
      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
        setState(() {
          _renderingSize = _measuringContainerKey.currentContext.size;
        });
      });

      // サイズ計測用のContainerを返す
      return Container(
        key: _measuringContainerKey,
      );
    }
    // サイズが判明しているのでInteractiveViewerを作成.
    return _buildInteractiveViewer();
  }

  /// InteractiveViewerを構築する
  Widget _buildInteractiveViewer() {
    // コンテンツのサイズ
    final contentSize = Size(300, 300);
    // コンテンツ本体
    final content = SizedBox(
      width: contentSize.width,
      height: contentSize.height,
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Colors.red, Colors.blue, Colors.red],
          ),
        ),
      ),
    );

    // 初期表示位置の調整(中心になるように)
    TransformationController controller = TransformationController()
      ..value = Matrix4.translationValues(
        min((_renderingSize.width - contentSize.width) / 2, 0),
        min((_renderingSize.height - contentSize.height) / 2, 0),
        0,
      );

    // InteractiveViewerよりコンテンツのほうが完全に大きい場合はそのまま渡す
    if (contentSize.width > _renderingSize.width &&
        contentSize.height > _renderingSize.height) {
      return InteractiveViewer(
        constrained: false,
        boundaryMargin: EdgeInsets.all(0),
        transformationController: controller,
        child: content,
      );
    }

    // SizedBoxの中でセンタリングさせたものを渡す
    return InteractiveViewer(
      constrained: false,
      boundaryMargin: EdgeInsets.all(0),
      transformationController: controller,
      child: SizedBox(
        width: max(_renderingSize.width, contentSize.width),
        height: max(_renderingSize.height, contentSize.height),
        child: Center(child: content),
      ),
    );
  }

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

    // WidgetsBindingObserverに登録されていなければ登録する
    if (!_observerRegistered) {
      _observerRegistered = true;
      WidgetsBinding.instance.addObserver(this);
    }
  }

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

    // WidgetsBindingObserverの登録を解除
    WidgetsBinding.instance.removeObserver(this);
  }

  @override
  void didChangeMetrics() {
    // ウィンドウサイズの変更をキャッチしたらレンダリングサイズを再計算する
    setState(() {
      _renderingSize = null;
    });
  }
}
```

## 課題

- Webでウィンドウの拡縮を行うと激しくチラつくので、Webでは実際にInteractiveViewerを描画させるまで多少時間を置いたほうが良いかもしれない
- 計算量多い気がするので、お詳しい方もっといい方法ご存じならマサカリください。

## 所感

- かなり力技なのでもう少しスマートなやり方があってほしい(InteractiveViewerを自作する以外で)
- ~~Alignmentみたいなフィールドが追加されたら完全に意味なくなる記事だなぁと思いながら書きました~~
- 初期位置のセンタリングもついでに行えて良かった

■わかりにくいが横軸でセンタリングされている
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700353/13c18fa7-a304-e1d0-69c5-49ddbdaab86f.png" width=300>