えんじにあ雑記!

開発していて学んだことをまとめていきます!

ContentSizeFilterを実装してみる

f:id:flat-M_M:20210314181821j:plain

UnityのLayoutControllerの一つであるContentSizeFilterの挙動を、実際に実装しながら理解してみました。

はじめに

uGUIの内部実装はIDEの「実装元に移動」や、BuiltInPackageディレクトリに保存されているcom.unity.uguiあたりから見ることができます。

主目的は内部の挙動を理解することなので、今回実装するコンポーネントの利便性は考慮せず、なるべく要点だけを実装しています。

前説

UIBehaviourについて

ContentSizeFilterはUIBehaviourというクラスを継承していますが、このUIBehaviourとは公式リファレンスより

Unity ライフサイクルに則った UI のベースとなる Behaviour クラス

と表記されています。MonoBehaviourを継承しつつ、RectTransformの値が変化した際のコールバックメソッドなどが定義されており、uGUIのライフサイクルに適して使いやすいクラスとなっています。

docs.unity3d.com

ContentSizeFilterでもRectTransformの値の変化を検知したり、MonoBehaviourのライフサイクルで呼ばれるメソッドをオーバライドするためにこのUIBehaviourを継承しています。

ILayoutSelfControllerについて

ILayoutSelfControllerは下記の2つのメソッドが定義されたインタフェースで、自分自身のRectTransformを操作するLayoutControllerに必要なインタフェースとなっています。

  • SetLayoutHorizontal
  • SetLayoutVertical

キーワード

ContentSizeFilterを実装するにあたり、下記のキーワードが重要かと思います。

  • LayoutRebuilder.MarkLayoutForRebuild
  • DrivenRectTransformTracker
  • LayoutElementのMin,PreferredSizeについて

実装解説

3つのFitMode定義

ContentSizeFilterには3つのFitModeが存在します。

  • Unconstrained
  • MinSize
  • PreferredSize

これらのFitModeは上から

  • サイズをレイアウト要素に連動させない
  • サイズをMinに連動させる
  • サイズをPreferredに連動させる

という機能になります。

これらをまずはenumで定義するところから始めます。

[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
public class MyContentSizeFilter : UIBehaviour, ILayoutSelfController
{
    private enum FitMode
    {
        Unconstrained,
        MinSize,
        PreferredSize
    }

    [SerializeField]
    private FitMode horizontalFit = FitMode.Unconstrained;

    [SerializeField]
    private FitMode verticalFit = FitMode.Unconstrained;
}

SetDirtyについて

次に、ContentSizeFilterの設定が変更され、レイアウトの再ビルドが必要であることを通知するためのメソッドであるSetDirtyを実装します。

SetDirtyメソッド内部では、自分自身がActiveな状態である場合に、LayoutRebuilderのMarkLayoutForRebuildを呼び出すことで、レイアウトの再ビルドが必要であることを伝えます。

これにより、フレームの最後、レンダリングの直前でUIのレイアウトが再ビルドされるようになります。

protected void SetDirty()
{
    if (!IsActive())
    {
        return;
    }
    
    LayoutRebuilder.MarkLayoutForRebuild(RectTransform);
}

各メソッドのオーバーライド

  • OnEnable
  • OnDisable
  • OnRectTransformDimensionsChange

の3つのメソッドをオーバーライドしていきます。

レイアウトの再ビルドが必要であることを伝えます。

protected override void OnEnable()
{
    base.OnEnable();
    SetDirty();
}

protected override void OnDisable()
{
    LayoutRebuilder.MarkLayoutForRebuild(RectTr);
    base.OnDisable();
}

protected override void OnRectTransformDimensionsChange()
{
    SetDirty();
}

ILayoutSelfControllerの実装

ILayoutSelfControllerには2つのメソッドが定義されており、それらを実装します。

public void SetLayoutHorizontal()
{
    HandleSelfFittingAlongAxis(0);
}

public void SetLayoutVertical()
{
    HandleSelfFittingAlongAxis(1);
}

HandleSelfFittingAlongAxisの実装

ILayoutSelfControllerに定義されたメソッド内で呼び出しているHandleSelfFittingAlongAxisメソッドの実装を行います。

private void HandleSelfFittingAlongAxis(int axis)
{
    var fitMode = (axis == 0 ? horizontalFit : verticalFit);
    if (fitMode == FitMode.Unconstrained)
    {
        return;
    }
    

    if (fitMode == FitMode.MinSize)
    {
        RectTr.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetMinSize(RectTr, axis));
    }
    else
    {
        RectTr.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetPreferredSize(RectTr, axis));
    }
}

まず、引数のaxisから軸を判定し、設定されているFItModeを取得します。

UnConstrainedの場合は何もせず処理を終了します。

MinSize,PreferredSizeの場合はLayoutUtility.GetOOSizeのメソッドを利用して、軸に対してサイズを計算し、それを自分自身のRectTransformのサイズに設定します。

DrivenRectTransformTrackerを使用する

現時点でContentSizeFilterとして機能するコンポーネントは実装することができていますが、ここでもう一点だけ解説します。

現状の自作ContentSizeFilterをアタッチすると、HeightやWidthにロックはかかっておらず、一見するとInspectorViewから変更できそうになっています。

f:id:flat-M_M:20210314181246p:plain

これを、変更できないように設定していこうと思います。

方法としては、DrivenRectTransformTrackerに制御するRectTransformを登録してあげることで実現します。

private DrivenRectTransformTracker _tracker;

protected override void OnDisable()
{
    // Disableのタイミングでclearを呼び出す必要がある
    _tracker.Clear();
    LayoutRebuilder.MarkLayoutForRebuild(RectTr);
    base.OnDisable();
}

private void HandleSelfFittingAlongAxis(int axis)
{
    var fitMode = (axis == 0 ? horizontalFit : verticalFit);
    if (fitMode == FitMode.Unconstrained)
    {
        // どの要素もトラックしない設定で追加する
        _tracker.Add(this, RectTr, DrivenTransformProperties.None);
        return;
    }
    // 軸に合わせてトラックする箇所を決めて追加する
    _tracker.Add(this, RectTr, (axis == 0 ? DrivenTransformProperties.SizeDeltaX : DrivenTransformProperties.SizeDeltaY));

    if (fitMode == FitMode.MinSize)
    {
        RectTr.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetMinSize(RectTr, axis));
    }
    else
    {
        RectTr.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetPreferredSize(RectTr, axis));
    }
}

public void SetLayoutHorizontal()
{
    // レイアウトのサイズ決定はwidthから行われるため、このタイミングでClearする
    _tracker.Clear();
    HandleSelfFittingAlongAxis(0);
}