Skip to content

Instantly share code, notes, and snippets.

@garrynewman
Created May 22, 2019 19:11
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save garrynewman/80ff4093af0efbffb641e0df201fc249 to your computer and use it in GitHub Desktop.
Save garrynewman/80ff4093af0efbffb641e0df201fc249 to your computer and use it in GitHub Desktop.
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