問題点
下記を満たしている時、コンテンツが左上に張り付きます。
こいつをセンタリングさせるのが目標です。
- InteractiveViewerのサイズが中身のコンテンツのサイズを超えている
- constrained=falseが設定されている
ちなみに、Flutter for Webでは (→Androidでも再現しました) その状態で少し拡大したらminScale/maxScaleを無視してInteractiveViewer全体にコンテンツが広がります。バグの香りがします。
→起票してみました
InteractiveViewerとは
比較的最近追加された、UIScrollViewみたいなものです。
SingleChildScrollViewなどは一方向のスクロールですが、InteractiveViewerは縦横スクロールに対応しています。
以下の記事に詳しくまとめていただいている方が居ます。
Flutter InteractiveViewerの使い方
対処方針
今回対象とするコンテンツの要件は以下の通りです。
- ユーザーによる拡縮はOK、初期表示で拡大されているのはNG
- 画面幅が余った際はコンテンツをセンタリングさせて等倍で表示
InteractiveViewerにalignmentみたいなフラグがあればそれで終わる話なのですが、無さそうなので中身のコンテンツのサイズを調整して解決する事にしました。
ただし、InteractiveViewerでconstrained=falseをセットしていると、内側からはちょうどいいサイズに調整しようにも求められるサイズが取得できないため少し遠回りな処理となっています。
- 仮のContainerを設置する
- レンダリングが終わったらサイズを取得する
- 本命のInteractiveViewerを設置する
流れとしてはこんな感じで処理します。あと画面サイズの変更(回転・ブラウザの拡縮)の際には再計算させています。
中身のコンテンツのサイズが分からない場合はそちらも計算してやる必要がありますが同じような形で実装できると思います。
対処コード
動作確認はAndroid/Webで行っています。
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みたいなフィールドが追加されたら完全に意味なくなる記事だなぁと思いながら書きました- 初期位置のセンタリングもついでに行えて良かった