Unityで画像とラベルを同時にLayoutしたい時

# TL;DR: Layout Elementを画像側につけて、必要なサイズを定義→親にLayout GroupとContent Size Fitter

Layout Elementはそれ自体は何もしないが、Layout Groupと組み合わさった時に裏で発動し、
Layout Groupが利用するサイズ情報を上書いてくれるようになる。

https://docs.unity3d.com/ja/2018.4/Manual/script-LayoutElement.html

読めば書いてあることだが、意外と気づかない機能だったので共有。

それぞれのプロパティの意味は

* Min: 絶対にこれより小さくなって欲しくない大きさ
* Preferred: 無理でない限り指定したい大きさ
* Flexible: 余裕があった時に埋めたい大きさを、親要素の大きさに対する割合で指定

`Preferred` で強制的に*画像側の*サイズを決定したら、親要素に`Content Size Fitter`を入れて`Preferred Size`を適用すれば、問題なく表示される。ラベルにElementを追加する必要はない。

# 要素の大きさが違うだけで困った

このように「キーボードのキートップの絵(Raw Image)」と「そのキーが何をするかを書いたラベル(TMPUGUI)」を組み合わせて操作説明のUIを作ることを考える。(生憎購入したキートップアセットが`Texture`になっていたため`Raw Image`を利用しているが、`Image`でも同じ問題が起こりうる)
[![Image from Gyazo](https://i.gyazo.com/9e2b02cb927ab1ef1ad0803214655121.png)](https://gyazo.com/9e2b02cb927ab1ef1ad0803214655121)

ひとまず、キートップとラベルを親要素でまとめて作ってみよう。
この時操作説明の文章の文量が変わらないのなら静的に作ってしまってもいいが、例えば他言語対応するとなると、それぞれの言語でテキストの幅が変わってしまって微妙な隙間ができて悲しいことになる。
[![Image from Gyazo](https://i.gyazo.com/5853d18590ec44977338b49e73e57bd3.png)](https://gyazo.com/5853d18590ec44977338b49e73e57bd3)
翻訳した結果が元の文字列の幅より長かったらもっと悲惨だ(日本語を先に作った時など)。
[![Image from Gyazo](https://i.gyazo.com/56d165b4ee1fac7c07e1a08dca4e986f.png)](https://gyazo.com/56d165b4ee1fac7c07e1a08dca4e986f)

これは流石に自動化したいところ。

# Horizontal Layout Groupが荒ぶる

こういう時こそと思ってHorizontal Layout Groupを親GameObjectに適用してみたら、余計に悲しいことになった。キートップが消滅したのである。
[![Image from Gyazo](https://i.gyazo.com/bcb630a483fa074edb0dcab86ddde919.png)](https://gyazo.com/bcb630a483fa074edb0dcab86ddde919)
ラベルも全く正しいサイズになっていない。

# Layout Element + Content Size Fitterが正解

Layout ElementとContent Size Fitterが今回の正解だ。特にLayout Elementはこれ単体では何もしないように見えるコンポーネントなので、先にLayout Groupを見つけてしまった場合は特にElementの存在に気づかないかもしれない。

キーボードのキートップの絵は変えなくていい。ということは、幅も変わらない。従って、絵の方には `Preferred Width`が定義できる。合わせて、`Height`の方も念のため入れておこう(なぜ`Min`にしないのかはあとで解説)。

[![Image from Gyazo](https://i.gyazo.com/9f9dc626c500c53d704b13f18a45e09c.png)](https://gyazo.com/9f9dc626c500c53d704b13f18a45e09c)

ラベルの方はそのままでいい。この場合は、「**そのGameObjectについている描画系コンポーネント(この場合TextやTMPUGUI)が報告する値をそのまま使う**」という動作になる。 Inspectorの一番下に「Layout Properties」というタブが隠れていて、上にドラッグすると見えるようになる。ここに書かれている値が親側で使われる値となる(`Source`が`TextMeshProUGUI`であることに注目)。

[![Image from Gyazo](https://i.gyazo.com/ab3fea3cc2c68736164b89461cc8314c.png)](https://gyazo.com/ab3fea3cc2c68736164b89461cc8314c)

最後に、親要素に対して、Layout Group(今回は `Horizontal Layout Group`)を入れ、`Control Child Size`を `Width` に関して有効にする。その上で、**Content Size Fitter**も同時に適用して `Horizontal Fit` を `Preferred Size` にする。こうすることで、Elementを入れただけで放置していたラベルが裏で計算してLayout Groupに伝えていた`Preferred Width`を親要素に対して適用してくれる。

[![Image from Gyazo](https://i.gyazo.com/30386b06a1c9d681d846179cf8d48ecd.png)](https://gyazo.com/30386b06a1c9d681d846179cf8d48ecd)

ちなみに:
* Horizontal Layout Groupの`Contorol Child Size`→`Width`が付いてない場合
  →ラベル要素が伸び縮みしない
* Content Size Fitterを使わない場合や、`Horizontal Fit`を`Unconstrained`や`Min Size`にした場合
  →親要素が伸び縮みしないので、UnityのシーンEdit時に定義したより長いラベルになったときに変な改行ができてしまう
  →Min Sizeにした場合、キートップは消滅しラベルの文字が縦書きになる(後述)

という不具合が起こる。

# そもそも何が問題だったのか？

先ほど少し紹介した `Layout Properties`、ここに答えが隠れていた。Layout Elementを使わない場合のPropertiesの数値は以下の通りになっていた。

||Preferredのサイズ|
|:--|:--:|
|Image|`Source Image`に指定している`Sprite`の元のサイズ|
|Raw Image|**0 x 0**|
|ラベル(TMPUGUI)|ラベルの内容が指定された通りに表示できる最小サイズ|

(Minは全て0 x 0、Flexibleは全てdisabled)

実は、**これらがLayout Group系のコンポーネントがレイアウトを計算するときに見ている数字である**。

Content Size Fitterは小要素の(*報告した*)`Min Size` ないしは `Preferred Size`を見ることでLayout Groupを持っている要素のサイズを計算することができる。だが、このままだとHorizontal Layout Groupを入れてContent Size Fitterを使ったとしても、全てを一発で適切なサイズに合わせられる設定がない。Minだと全て 0 x 0 になってしまうし、Preferredなら Imageは原寸、Raw Imageは潰れてしまう。  
そこで、**Text系の`Preferred`が自動計算されていることを利用して、ImageコンポーネントにLayout ElementコンポーネントをつけてこのGameObjectがContent Size Fitterに*報告する*`Preferred`を上書きし、Fitterに`Preferred`の値を使ってサイズ変更させるようにする**ことで、自動で長さを変える機能を実現したわけである。

Content Size Fitterの設定を `Min Size` にしたときにおかしな見た目になる理由も、これで説明がつくだろう。全てが0 x 0になっているのだ。上書きできるとはいえ、せっかくPreferred Sizeが自動計算されるラベル系コンポーネントの旨味が活かせない。

Layoutで困ったら、このLayout Propertiesを覗いて誰がなんと報告しているのかを見るのが一番近道かもしれない。

# ちなみに

この辺の説明は次のサイトがわかりやすく解説しています:

https://tsubakit1.hateblo.jp/entry/2017/05/14/163456