Created
May 22, 2019 19:11
-
-
Save garrynewman/80ff4093af0efbffb641e0df201fc249 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Linq; | |
using UnityEngine; | |
namespace Facepunch | |
{ | |
public class VirtualScroll : MonoBehaviour | |
{ | |
// | |
// Implement this for shit | |
// | |
public interface IDataSource | |
{ | |
int GetItemCount(); | |
void SetItemData( int i, GameObject obj ); | |
} | |
public int ItemHeight = 40; | |
public int ItemSpacing = 10; | |
public RectOffset Padding; | |
public GameObject SourceObject; | |
public UnityEngine.UI.ScrollRect ScrollRect; | |
private IDataSource dataSource; | |
private Dictionary<int, GameObject> ActivePool = new Dictionary<int, GameObject>(); | |
private Stack<GameObject> InactivePool = new Stack<GameObject>(); | |
public void Awake() | |
{ | |
ScrollRect.onValueChanged.AddListener( OnScrollChanged ); | |
} | |
public void OnDestroy() | |
{ | |
ScrollRect.onValueChanged.RemoveListener( OnScrollChanged ); | |
} | |
void OnScrollChanged( Vector2 pos ) | |
{ | |
Rebuild(); | |
} | |
/// <summary> | |
/// This should be called to set the scroller to your custom data source. | |
/// </summary> | |
public void SetDataSource( IDataSource source ) | |
{ | |
if ( dataSource == source ) return; | |
dataSource = source; | |
FullRebuild(); | |
} | |
int BlockHeight => ItemHeight + ItemSpacing; | |
/// <summary> | |
/// Completely scrap everything and rebuild the layout again. | |
/// You probably want to do this if items have been removed, | |
/// or you're changing padding /spacing etc. | |
/// </summary> | |
public void FullRebuild() | |
{ | |
foreach ( var key in ActivePool.Keys.ToArray() ) | |
{ | |
Recycle( key ); | |
} | |
Rebuild(); | |
} | |
/// <summary> | |
/// Call this if it's likely that the visible items have changed | |
/// Like if there's been a re-order, or new data inserted. We'll | |
/// make the visible items update their data. | |
/// </summary> | |
public void DataChanged() | |
{ | |
foreach ( var key in ActivePool ) | |
{ | |
dataSource.SetItemData( key.Key, key.Value ); | |
} | |
Rebuild(); | |
} | |
/// <summary> | |
/// Usually only really need to call this when scrolling around | |
/// </summary> | |
public void Rebuild() | |
{ | |
if ( dataSource == null ) return; | |
var items = dataSource.GetItemCount(); | |
var canvas = ScrollRect.viewport.GetChild( 0 ) as RectTransform; | |
canvas.SetSizeWithCurrentAnchors( RectTransform.Axis.Vertical, BlockHeight * items - ItemSpacing + Padding.top + Padding.bottom ); | |
var maxItemsVisible = Mathf.Max( 2, Mathf.CeilToInt( ScrollRect.viewport.rect.height / BlockHeight ) ); | |
var startVisible = Mathf.FloorToInt( (canvas.anchoredPosition.y - Padding.top) / BlockHeight ); | |
var endVisible = startVisible + maxItemsVisible; | |
RecycleOutOfRange( startVisible, endVisible ); | |
for ( int i = startVisible; i <= endVisible; i++ ) | |
{ | |
if ( i < 0 ) continue; | |
if ( i >= items ) continue; | |
BuildItem( i ); | |
} | |
} | |
void RecycleOutOfRange( int startVisible, float endVisible ) | |
{ | |
var notVisible = ActivePool.Keys | |
.Where( x => x < startVisible || x > endVisible ) | |
.Select( x => x ) | |
.ToArray(); | |
foreach ( var key in notVisible ) | |
{ | |
Recycle( key ); | |
} | |
} | |
void Recycle( int key ) | |
{ | |
var obj = ActivePool[key]; | |
obj.SetActive( false ); | |
ActivePool.Remove( key ); | |
InactivePool.Push( obj ); | |
} | |
void BuildItem( int i ) | |
{ | |
if ( i < 0 ) return; | |
if ( ActivePool.ContainsKey( i ) ) return; | |
var item = GetItem(); | |
item.SetActive( true ); | |
dataSource.SetItemData( i, item ); | |
// | |
// This fucking UI system man, fuck me | |
// | |
var rt = item.transform as RectTransform; | |
rt.anchorMin = new Vector2( 0, 1 ); | |
rt.anchorMax = new Vector2( 1, 1 ); | |
rt.pivot = new Vector2( 0.5f, 1 ); | |
rt.offsetMin = new Vector2( 0, 0 ); | |
rt.offsetMax = new Vector2( 0, ItemHeight ); | |
rt.sizeDelta = new Vector2( (Padding.left + Padding.right) * -1, ItemHeight ); | |
rt.anchoredPosition = new Vector2( (Padding.left - Padding.right) * 0.5f, -1 * (i * BlockHeight + Padding.top) ); | |
ActivePool[i] = item; | |
} | |
GameObject GetItem() | |
{ | |
if ( InactivePool.Count == 0 ) | |
{ | |
var go = GameObject.Instantiate( SourceObject ); | |
go.transform.SetParent( ScrollRect.viewport.GetChild( 0 ), false ); | |
go.transform.localScale = Vector3.one; | |
go.SetActive( false ); | |
InactivePool.Push( go ); | |
} | |
return InactivePool.Pop(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment