効率的なスクロールビューを実装するためのOSS『Loop Scroll Rect』使い方をまとめました。
- Loop Scroll Rectとは?
- シンプルなスクロールビューを実装する
- オブジェクトを使い回す
- 無限ループするビューを作る
- 要素ごとに高さを変える
- 要素ごとにPrefabを変える
- グリッドスクロールビューを実装する
- 参考
Unity 2020.3.15f2
Loop Scroll Rect 1.1.0
Loop Scroll Rectとは?
Unity標準のスクロールビューは、要素の数が増えるほど処理負荷とメモリの使用量が大きくなるため、スケール可能にするためには工夫が必要です。
例えば表示範囲外のGameObjectを非アクティブにしたり、オブジェクトプールを使って使い回したりすることが考えられます。
Loop Scroll Rectを使うとこのような効率的なスクロールビューを簡単に実現することができます。
日本ではあまり使われてなさそうな気がしますが、現時点で約1400Starsの人気ライブラリで、更新頻度も高めです。
このライブラリはMITライセンスのOSSとして公開されています。
インストールするにはPackage Managerから以下のURLを入力します。
シンプルなスクロールビューを実装する
スクロールビューを作成する
さてそれではまずシンプルな縦スクロールビューを実装してみます。
横スクロールビューは割愛しますが、基本的には縦と同じ手順で作成できます。
最初にGameObject > UI > Loop Vertical Scroll Rect
からセットアップ済みのGameObjectを生成します。
このGameObjectのルートがスクロールビューの範囲となるので、大きさを適切に設定しておきます。
次その子GameObjectであるContentの設定をします。
Vertical Layout Group
がアタッチされているので、Control Child Size
のHeightにチェックを入れておきます。
その他の設定は任意で、通常のVertical Layout Group
と同様に行えます。
要素のPrefabを作成する
次にその子としてスクロールビューの要素となるGameObjectを作成していきます。
Loop Scroll Rect
では、スクロールビューの高さをこの要素のPreferred Height
を使って計算します。
したがってLayout Element
などを使ってPreferred Height
を設定します。
Text Mesh Pro
などのオブジェクトはLayout Element
がなくてもPreferred Height
が計算されるため、このあたりは必要に応じて設定するようにしてください。
要素が作れたらこれをPrefabにしておきます。
今回は下図のようにImageとTextで構成されるPrefabを作りました。
スクリプトを作成する
次にLoopScrollRect
を初期化するためのスクリプトを作成します。
以下のようにLoopScrollPrefabSource
とLoopScrollDataSource
を実装したオブジェクトをLoopScrollRect
にアサインします。
using TMPro; using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(LoopScrollRect))] [DisallowMultipleComponent] public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollDataSource { [SerializeField] private GameObject _prefab; public int totalCount = -1; private void Start() { var scrollRect = GetComponent<LoopScrollRect>(); scrollRect.prefabSource = this; scrollRect.dataSource = this; scrollRect.totalCount = totalCount; scrollRect.RefillCells(); } // LoopScrollPrefabSourceの実装 // GameObjectが新しく表示のために必要になった時に呼ばれる GameObject LoopScrollPrefabSource.GetObject(int index) { // 今回は毎回Instantiate(プールしてない) return Instantiate(_prefab); } // LoopScrollPrefabSourceの実装 // GameObjectが不要になった時に呼ばれる void LoopScrollPrefabSource.ReturnObject(Transform trans) { // 今回は毎回Destroy(プールしてない) DestroyImmediate(trans.gameObject); } // LoopScrollDataSourceの実装 // 要素が表示される時の処理を書く void LoopScrollDataSource.ProvideData(Transform trans, int index) { trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = index.ToString(); } }
LoopScrollPrefabSource
はLoopScrollRect
からGameObjectが要求・返却された時の処理を定義します。
表示に使わないGameObjectを非アクティブにしたり、オブジェクトプーリングをしたい時にはこの辺りに処理を書きます。
LoopScrollDataSource
には各要素を表示する際のセットアップ処理を定義します。
動作確認
それでは一度動作確認をしてみます。
上記のコンポーネントをLoop Scroll Rect
コンポーネントと同じGameObjectにアタッチし、Prefabプロパティに先ほど作ったPrefabをアサインします。
TotalCount
は100に設定しておきます。
これで再生すると以下のような結果が得られます。
なおついでにスクロールバーを設定していますが、これの設定方法は通常のScrollRectと同様の設定方法なので割愛します。
オブジェクトを使い回す
さてここまでの実装では画面内に入るたびにGameObjectをインスタンス化し、画面外に出るたびに破棄していました。
これは非効率なのでオブジェクトを使い回すことで効率化します。
Unityでは2021からオブジェクトプールが実装されたので、今回はこれを使ってみます。
実装は以下の通りです。
using TMPro; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UI; [RequireComponent(typeof(LoopScrollRect))] [DisallowMultipleComponent] public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollDataSource { [SerializeField] private GameObject _prefab; public int totalCount = -1; private ObjectPool<GameObject> _pool; private void Start() { // オブジェクトプールを作成 _pool = new ObjectPool<GameObject>( // オブジェクト生成処理 () => Instantiate(_prefab), // オブジェクトがプールから取得される時の処理 o => o.SetActive(true), // オブジェクトがプールに戻される時の処理 o => { o.transform.SetParent(transform); o.SetActive(false); }); var scrollRect = GetComponent<LoopScrollRect>(); scrollRect.prefabSource = this; scrollRect.dataSource = this; scrollRect.totalCount = totalCount; scrollRect.RefillCells(); } void LoopScrollDataSource.ProvideData(Transform trans, int index) { trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = index.ToString(); } GameObject LoopScrollPrefabSource.GetObject(int index) { // オブジェクトプールからGameObjectを取得 return _pool.Get(); } void LoopScrollPrefabSource.ReturnObject(Transform trans) { // オブジェクトプールにGameObjectを返却 _pool.Release(trans.gameObject); } }
これでオブジェクトが使い回されるようになりました。
無限ループするビューを作る
さてここまでの実装では、スクロールし続けてTotalCount
に設定した数が表示された時点でスクロールが止まります。
このときスクロールを止めずに最初の要素にループさせるには、TotalCount
に-1を入力します。
またこの場合、LoopScrollDataSource.ProvideData
に渡されるインデックスの値は無限に増えていくため、そこから以下のようにデータのインデックスを求める必要があります(コメント部分)。
using TMPro; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UI; [RequireComponent(typeof(LoopScrollRect))] [DisallowMultipleComponent] public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollDataSource { [SerializeField] private GameObject _prefab; public int totalCount = -1; // データ数 public int dataCount = 100; private ObjectPool<GameObject> _pool; private void Start() { _pool = new ObjectPool<GameObject>( () => Instantiate(_prefab), o => o.SetActive(true), o => { o.transform.SetParent(transform); o.SetActive(false); }); var scrollRect = GetComponent<LoopScrollRect>(); scrollRect.prefabSource = this; scrollRect.dataSource = this; scrollRect.totalCount = totalCount; scrollRect.RefillCells(); } void LoopScrollDataSource.ProvideData(Transform trans, int index) { // データのインデックスを求める var dataIndex = (int)Mathf.Repeat(index, dataCount); trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = dataIndex.ToString(); } GameObject LoopScrollPrefabSource.GetObject(int index) { return _pool.Get(); } void LoopScrollPrefabSource.ReturnObject(Transform trans) { _pool.Release(trans.gameObject); } }
これを実行すると以下の結果が得られます。
要素がループしていることがわかります。
また上図の通り、無限ループを行う際にはスクロールバーが機能しませんのでご注意ください(機能したとしても挙動がわけわからなくなるのでそれはそう)。
要素ごとに高さを変える
要素ごとに高さを変えるには以下のようにLoopScrollDataSource.ProvideData
でPreferred Heightを変更します。
using TMPro; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UI; [RequireComponent(typeof(LoopScrollRect))] [DisallowMultipleComponent] public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollDataSource { [SerializeField] private GameObject _prefab; public int totalCount = -1; public int dataCount = 100; private ObjectPool<GameObject> _pool; private void Start() { _pool = new ObjectPool<GameObject>( () => Instantiate(_prefab), o => o.SetActive(true), o => { o.transform.SetParent(transform); o.SetActive(false); }); var scrollRect = GetComponent<LoopScrollRect>(); scrollRect.prefabSource = this; scrollRect.dataSource = this; scrollRect.totalCount = totalCount; scrollRect.RefillCells(); } void LoopScrollDataSource.ProvideData(Transform trans, int index) { var dataIndex = (int)Mathf.Repeat(index, dataCount); // 要素ごとに高さを変える var height = dataIndex % 2 == 0 ? 50 : 100; trans.GetComponent<LayoutElement>().preferredHeight = height; trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = dataIndex.ToString(); } GameObject LoopScrollPrefabSource.GetObject(int index) { return _pool.Get(); } void LoopScrollPrefabSource.ReturnObject(Transform trans) { _pool.Release(trans.gameObject); } }
実行結果は以下のようになります。今回は奇数のインデックスのセルと偶数のインデックスのセルでそれぞれ別の高さを設定しています。
要素ごとにPrefabを変える
次に要素ごとにPrefabを変更してみます。
複数のPrefabを使用する場合、Loop Vertical Scroll Rect
コンポーネントの代わりにLoop Vertical Scroll Rect Multi
コンポーネントを使用します。
InspectorをDebugモードにしてコンポーネントを差し替えるのが簡単です。
スクリプトは以下のように変更します。
LoopScrollDataSource
の代わりにLoopScrollMultiDataSource
を実装します。
その他の変更点はコメントとして記述しました。
using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UI; [RequireComponent(typeof(LoopScrollRect))] [DisallowMultipleComponent] public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollMultiDataSource // LoopScrollMultiDataSourceに変更 { // Prefabを複数指定できるように [SerializeField] private GameObject[] _prefabs; public int totalCount = -1; public int dataCount = 100; // PoolもPrefabと同じだけ生成 private ObjectPool<GameObject>[] _pools; private Dictionary<GameObject, int> _prefabIndexMap = new Dictionary<GameObject, int>(); private void Start() { // Prefabごとにプールを生成する _pools = new ObjectPool<GameObject>[_prefabs.Length]; for (var i = 0; i < _prefabs.Length; i++) { var prefab = _prefabs[i]; var pool = new ObjectPool<GameObject>( () => Instantiate(prefab), o => o.SetActive(true), o => { o.transform.SetParent(transform); o.SetActive(false); }); _pools[i] = pool; } // LoopScrollRectMultiに変更 var scrollRect = GetComponent<LoopScrollRectMulti>(); scrollRect.prefabSource = this; scrollRect.dataSource = this; scrollRect.totalCount = totalCount; scrollRect.RefillCells(); } // LoopScrollMultiDataSourceを実装 void LoopScrollMultiDataSource.ProvideData(Transform trans, int index) { var dataIndex = (int)Mathf.Repeat(index, dataCount); trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = dataIndex.ToString(); } GameObject LoopScrollPrefabSource.GetObject(int index) { // Indexに応じて違うPrefabを使う var prefabIndex = index % _prefabs.Length; var instance = _pools[prefabIndex].Get(); _prefabIndexMap.Add(instance, prefabIndex); return instance; } void LoopScrollPrefabSource.ReturnObject(Transform trans) { // Indexに応じたプールを取得して返却 var instance = trans.gameObject; var prefabIndex = _prefabIndexMap[instance]; _prefabIndexMap.Remove(instance); _pools[prefabIndex].Release(instance); } }
実行結果は下図の通りとなります。
今回は3種類のPrefabを設定しています。
グリッドスクロールビューを実装する
最後に要素をグリッドで並べたスクロールビューを実装します。
グリッドスクロールビューを使うためにはVertical Layout Group
の代わりにGrid Layout Group
を使用します。
大半のプロパティは任意ですが、ConstraintをFlexibleにすることはできません。
またグリッドスクロールビューの場合、セルの高さはPreferred Height
ではなくGrid Layout Group
のCell Size
から取得されます。
したがって各要素にLayout Element
をアタッチする必要はありません。
グリッドスクロールビューの実行結果は以下の通りとなります。
またグリッドスクロールビューの場合、セルの高さはPreferred HeightではなくGrid Layout GroupのCell Sizeから取得されます。
したがって各要素にLayout Elementをアタッチする必要はありません。
グリッドスクロールビューの実行結果は以下の通りとなります。