ScrollList

From Unify Community Wiki
Jump to: navigation, search

Author: Serge Billault


Contents

Notes

  • 2020/10/13 - The bug that affected only the non-pooled mode have been fixed.
  • 2020/10/14 - ScrollList Editor now displays the Pooled option (now enabled by default) and the List Item Template fields first.

Archive

ScrollList.zip

Archive Content

. An editor folder
. An editor script
. The ScrollList script

Modes supported

. Pooled
. Non pooled ( Dual Ranges )

Live demo: WebGL application with scroll list
The ScrollList was motivated by the fact that restricted platforms like WebGL offer limited memory and cpu usage in a single threaded environment.
Performances: 40 scroll lists in the same window under the ms on a 10 years old machine (pooled mode).

Compatibility

. Unity versions: Unity 5.6.1f1 through 2019.4. (Untested on Unity 2020+).
. Net compatibility level: 2.0 minimum - no dynamic methods.
. Unsafe code: None.
. Reflectivity: Minor / non critical. Editor only.
. Networking: None.

Description

. Non destructive (no refactoring needed) replacement of the standard UnityEngine.UI.ScrolRect for memory efficient and fast list items.
. Safely switch back & forth to the standard ScrollRect (inherit from ScrollRect).
. Place holders are automatically handled for you. If you specify an Item Template it will be used. If not, the Scroll List look for one in its content. If no template is available it will then switch to place holders.
. Upon launching the player, all remaining items found in its contents are ghosted, but not destroyed.


Scroll list pic 0.jpg

Usage

Select an existing ScrollRect in your project and alternate ScrollRect/ScrollList behaviour as shown in the screenshot below


Scroll list pic 1.jpg


Code ( ScrollListEditor.cs )

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
 
//****************************************************************************************
//
//****************************************************************************************
 
[ CustomEditor( typeof( ScrollList ), false ), CanEditMultipleObjects ] 
 
public class ScrollListEditor : Editor
{
    //************************************************************************************
    //
    //************************************************************************************
 
    private const string PROP_POOL       = "m_use_pool";
 
    private const string PROP_TMPL       = "m_template";
 
    private const string PROP_POOL_LABEL = "Pooled";
 
    private const string PROP_TMPL_LABEL = "Item template";
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void EditNonInheritedProperties()
    {
        ScrollList         scroll_list = target as ScrollList;
 
        SerializedProperty use_pool    = serializedObject.FindProperty( PROP_POOL );
 
        SerializedProperty template    = serializedObject.FindProperty( PROP_TMPL );
 
        use_pool.boolValue             = EditorGUILayout.Toggle       ( PROP_POOL_LABEL, use_pool.boolValue );
 
        template.objectReferenceValue  = EditorGUILayout.ObjectField  ( PROP_TMPL_LABEL, template.objectReferenceValue, typeof( RectTransform ), true );
 
        serializedObject.ApplyModifiedProperties();
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    public override void OnInspectorGUI() 
    {
        EditNonInheritedProperties();
 
        base.OnInspectorGUI();
    }
}

Code ( ScrollList.cs )

using System;
using System.Collections;
using System.Collections.Generic;
 
//****************************************************************************************
//
//****************************************************************************************
 
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.EventSystems;
 
//****************************************************************************************
//
//****************************************************************************************
 
#if UNITY_EDITOR
 
using UnityEditor;
 
#endif
 
//****************************************************************************************
//
//****************************************************************************************
 
public class ScrollList : ScrollRect
{
    //************************************************************************************
    //
    //************************************************************************************
 
    public interface IItemData
    {
        void Bind( ScrollList list, int item, object data );
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private struct Range
    {
        static public readonly Range Invalid = new Range( -1, -1 );
 
        public int first;
 
        public int last;
 
 
        public Range( int param_first, int param_last ) { first = param_first; last = param_last; }
 
        public bool Overlap( Range other )
        {
            if( last  < other.first ) return false;
 
            if( first > other.last  ) return false;
 
            return true;
        }
 
        static public Range Merge( Range lhs, Range rhs )
        {
            int first = Mathf.Min( lhs.first, rhs.first );
 
            int last  = Mathf.Max( lhs.last,  rhs.last  );
 
            return new Range( first, last );
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
#if UNITY_EDITOR
 
    [ MenuItem( "GameObject/UI/SCROLL_LIST/Alt ScrollRect <-> ScrollList", false, 10 ) ]
 
    static private void Menu_Replace( MenuCommand menuCommand )
    {
        if( Application.isPlaying ) return;
 
        Replace( ( menuCommand.context != null ) ? menuCommand.context as GameObject : null );
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    static private void Replace( GameObject trg )
    {
        ScrollRect replaced = ( trg != null ) ? trg.GetComponent< ScrollRect >() : null;
 
        if( replaced != null )
        {
            System.Type type_replaced  = replaced.GetType();
 
            System.Type type_replacing = ( type_replaced == typeof( ScrollRect ) ) ? typeof( ScrollList ) : typeof( ScrollRect );
 
            string  json = JsonUtility.ToJson( replaced );
 
            DestroyImmediate( replaced );
 
 
            ScrollRect replacing = ( ScrollRect )trg.AddComponent( type_replacing );
 
            if( replacing != null )
            {
                JsonUtility.FromJsonOverwrite( json, replacing );
            }
        }
    }
 
#endif
 
    //************************************************************************************
    // HideInInspector => custom drawn in custom editor
    //************************************************************************************
 
    [ SerializeField ] [ HideInInspector ] public  bool          m_use_pool    = true;
 
    [ SerializeField ] [ HideInInspector ] public  RectTransform m_template    = null;
 
    [ NonSerialized  ] private bool                              m_pooled      = false;
 
    [ NonSerialized  ] private RectTransform                     m_scroll_cont = null;
 
    [ NonSerialized  ] private Transform                         m_grp_tmpl    = null;
 
    [ NonSerialized  ] private Transform                         m_grp_pool    = null;
 
    [ NonSerialized  ] private Transform                         m_grp_ghosts  = null;
 
    [ NonSerialized  ] private readonly List< Transform >        m_pool        = new List< Transform >( 32 );
 
    [ NonSerialized  ] private readonly List< Transform >        m_items       = new List< Transform >( 32 );
 
    [ NonSerialized  ] private readonly List< IItemData >        m_binds       = new List< IItemData >( 32 );
 
    [ NonSerialized  ] private          IList                    m_datas       = null;
 
    [ NonSerialized  ] private float                             m_items_h     = 0.0f;
 
    [ NonSerialized  ] private Range                             m_range       = Range.Invalid;
 
    //************************************************************************************
    //
    //************************************************************************************
 
    public bool dirty { get; set; }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    public IList itemsDatas 
    { 
        get { return m_datas; }
 
        set 
        { 
            if( m_datas != value ) 
            { 
                m_datas  = value; 
 
                dirty    = true;
            } 
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void NormalizeTemplate()
    {
        if( m_template != null )
        {
            float item_h = m_template.rect.height;
 
            m_template.anchorMin     = new Vector2( 0.0f, 1.0f );
 
            m_template.anchorMax     = new Vector2( 1.0f, 1.0f );
 
            m_template.pivot         = new Vector2( 0.5f, 1.0f );
 
            m_template.localPosition = Vector3.zero;
 
            m_template.offsetMin     = Vector2.zero;
 
            m_template.offsetMax     = Vector2.zero;
 
            m_template.sizeDelta     = new Vector2( m_template.sizeDelta.x, item_h );
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private Transform CreateGroup( Transform parent, string name, bool active )
    {
        if( parent != null )
        {
            for( int chld = 0; chld < parent.childCount; ++chld ) 
            {
                Transform child = parent.GetChild( chld );
 
                if( string.Equals( child.name, name, StringComparison.Ordinal ) ) return child;
            }
        }
 
        Transform grp = new GameObject( name ).transform;
 
        grp.transform.SetParent ( parent, false );
 
        grp.gameObject.SetActive( active );
 
        return grp;
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void ResolveDependencies()
    {
        if( Application.isPlaying == false ) return;
 
        m_scroll_cont = content;
 
        if( m_template == null ) 
        {
            m_template = ( m_scroll_cont != null ) && ( m_scroll_cont.childCount > 0 ) ? m_scroll_cont.GetChild( 0 ) as RectTransform : null;
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void SetupDependencies()
    {
        if( Application.isPlaying == false ) return;
 
        Transform root = transform;
 
        Transform grps = CreateGroup( root, "groups",   false );
 
        m_grp_tmpl     = CreateGroup( grps, "template", false );
 
        m_grp_pool     = CreateGroup( grps, "pool",     false );
 
        m_grp_ghosts   = CreateGroup( grps, "ghosts",   false );
 
 
        if( m_template != null ) 
        {
            NormalizeTemplate();
 
            m_items_h   = m_template.rect.height;
 
            Scene scene = m_template.gameObject.scene;
 
            int   count = ( scene != null ) ? scene.rootCount : 0;
 
            if( count > 0 ) m_template.SetParent( m_grp_tmpl, false );
        }
 
 
        if( m_scroll_cont != null )
        {
            while( m_scroll_cont.childCount > 0 )
            {
                m_scroll_cont.GetChild( 0 ).SetParent( m_grp_ghosts, false );
            }
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    override protected void Awake() 
    { 
        base.Awake();
 
        ResolveDependencies();
 
        SetupDependencies  ();
 
        m_pooled = m_use_pool;
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    override protected void Start() 
    { 
        base.Start();
 
        if( m_template == null )
        {
            m_template = ScrollListPlaceHolders.CreateTemplate( m_grp_tmpl );
 
            itemsDatas = ScrollListPlaceHolders.datas;
 
            m_items_h  = ( m_template != null ) ? m_template.rect.height : 0.0f;
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private Transform Instanciate()
    {
        Transform instance = ( m_template != null ) ? Instantiate( m_template.gameObject ).transform : null;
 
        return    instance;
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private Transform GrabExisting()
    {
        Transform existing = null;
 
        if( m_pool.Count > 0 ) 
        { 
            int last = m_pool.Count - 1; 
 
            existing = m_pool[ last ]; 
 
            m_pool.RemoveAt  ( last ); 
        }
 
        return existing;
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private Transform Grab()
    {
        Transform existing = GrabExisting();
 
        Transform instance = ( existing != null ) ? existing : Instanciate();
 
        if( instance != null ) 
        {
            instance.SetParent( m_scroll_cont.transform, false );
 
            instance.gameObject.SetActive( true );
        }
 
        return instance;
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void Release( Transform instance )
    {
        if( instance != null ) 
        {
            instance.gameObject.SetActive( false );
 
            instance.SetParent( m_grp_pool.transform, false );
 
            m_pool.Add( instance );
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private int UpdateItemsCount()
    {
        if( m_scroll_cont != null )
        {
            int nb_items = ( m_datas != null ) ? m_datas.Count : 0;
 
            m_scroll_cont.sizeDelta = new Vector2( m_scroll_cont.sizeDelta.x, nb_items * m_items_h );
 
            return nb_items;
        }
 
        return 0;
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private Range UpdateRange( int nb_items )
    {
        float scroll      = Mathf.Clamp01( 1.0f - normalizedPosition.y );
 
        float cont_h      = Mathf.Abs    ( ( m_scroll_cont != null ) ? m_scroll_cont.rect.height : 0.0f );
 
        float view_h      = Mathf.Abs    ( ( transform as RectTransform ).rect.height );
 
        float top         = ( cont_h   > view_h ) ? scroll * ( cont_h - view_h )              : 0.0f;
 
        int   visible_max = ( m_items_h > 0.0f  ) ? Mathf.CeilToInt( view_h / m_items_h ) + 1 : 0;
 
        int   visible_nb  = Mathf.Min( nb_items, visible_max );
 
        int   first       = ( visible_nb > 0 ) ? ( int )( top / m_items_h )                    :  0;
 
        int   last        = ( visible_nb > 0 ) ? Mathf.Min( first + visible_nb, nb_items ) - 1 : -1;
 
        return new Range( first, last );
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void PoolItems( Range range )
    {
        int nb_needed = ( range.last - range.first ) + 1;
 
        if( nb_needed != m_items.Count )
        {
            int nb_to_create = nb_needed - m_items.Count;
 
            int nb_to_delete = m_items.Count - nb_needed;
 
            if( nb_to_create > 0 ) 
            { 
                for( int itm = 0; itm < nb_to_create; ++itm ) 
                { 
                    Transform  item = Grab(); 
 
                    if( item == null ) break;
 
                    m_items.Add( item ); 
 
                    m_binds.Add( item.GetComponent< IItemData >() ); 
                } 
            }
            else
            { 
                for( int itm = 0; itm < nb_to_delete; ++itm ) 
                { 
                    int last = m_items.Count - 1;
 
                    Release( m_items[ last ] ); 
 
                    m_items.RemoveAt( last ); 
 
                    m_binds.RemoveAt( last );
                }
            }
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void LayoutPooledItems( Range range )
    {
        if( m_range.Equals( range ) == false )
        {
            for( int slot = 0; slot < m_items.Count; ++slot )
            {
                Transform item = m_items[ slot ];
 
                int       itm  = range.first + slot;
 
                if( item != null ) item.localPosition = new Vector3( item.localPosition.x, -m_items_h * itm );
            }
 
            m_range = range;
 
            dirty   = true;
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void ListItems( int nb_items )
    {
        int count  = m_items.Count;
 
        if( count != nb_items )
        {
            PoolItems( new Range( 0, nb_items - 1 ) );
 
            for( int itm = count; itm < m_items.Count; ++itm ) 
            {
                Transform item = m_items[ itm ];
 
                if( item != null ) 
                {
                    item.gameObject.SetActive( false );
 
                    item.localPosition = new Vector3( item.localPosition.x, -m_items_h * itm );
                }
            }
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void LayoutListedItems( int count, Range range )
    {
        if( m_range.Equals( range ) == false )
        {
            m_range.first = Math.Min( m_range.first, count - 1 );
 
            m_range.last  = Math.Min( m_range.last,  count - 1 );
 
 
            if( m_range.Overlap( range ) )
            {
                Range  merge = Range.Merge( range, m_range );
 
                for( int itm = Mathf.Max( 0, merge.first ); itm <= merge.last; ++itm ) 
                { 
                    m_items[ itm ].gameObject.SetActive( ( itm >= range.first ) && ( itm <= range.last ) ); 
                }
            }
            else
            {
                for( int itm = Mathf.Max( 0, m_range.first ); itm <= m_range.last; ++itm ) { m_items[ itm ].gameObject.SetActive( false ); } 
 
                for( int itm = Mathf.Max( 0,   range.first ); itm <=   range.last; ++itm ) { m_items[ itm ].gameObject.SetActive( true  ); }
            }
 
            m_range = range;
 
            dirty   = true;
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    private void BindItems( Range range )
    {
        if( dirty == true )
        {
            dirty = false;
 
 
            if( m_pooled )
            {
                for( int itm = Mathf.Max( 0, range.first ); itm <= range.last; ++itm )
                {
                    IItemData item_data = m_binds[ itm - range.first ];
 
                    if( item_data != null ) item_data.Bind( this, itm, m_datas[ itm ] );
                }
            }
            else
            {
                for( int itm = Mathf.Max( 0, range.first ); itm <= range.last; ++itm )
                {
                    IItemData item_data = m_binds[ itm ];
 
                    if( item_data != null ) item_data.Bind( this, itm, m_datas[ itm ] );
                }
            }
        }
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    public void Update()
    {
        if( Application.isPlaying == false ) return;
 
        int   count = UpdateItemsCount();
 
        Range range = UpdateRange( count );
 
 
        if( m_pooled ) 
        { 
            PoolItems        ( range ); 
 
            LayoutPooledItems( range ); 
        }
        else             
        { 
            ListItems        ( count ); 
 
            LayoutListedItems( count, range ); 
        }
 
        BindItems( range );
    }
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
//****************************************************************************************
//
//****************************************************************************************
 
static public class ScrollListPlaceHolders
{
    //************************************************************************************
    //
    //************************************************************************************
 
    static public readonly List< object > datas = new List< object >( 4096 );
 
    //************************************************************************************
    //
    //************************************************************************************
 
    static ScrollListPlaceHolders()
    {
        for( int itm = 0; itm < datas.Capacity; ++itm ) datas.Add( new object() );
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    static public RectTransform CreateTemplate( Transform parent )
    {
        RectTransform template = null;
 
        if( parent != null )
        {
            GameObject     tpl = new GameObject( "item_template", typeof( RawImage ), typeof( ScrollListItem ) );
 
            GameObject     lbl = new GameObject( "label", typeof( Text ) );
 
            ScrollListItem itm = tpl.GetComponent< ScrollListItem >();
 
            RawImage       img = tpl.GetComponent< RawImage       >();
 
            Text           txt = lbl.GetComponent< Text           >();
 
            tpl.transform.SetParent( parent,        false );
 
            lbl.transform.SetParent( tpl.transform, false );
 
 
            RectTransform xform = tpl.transform as RectTransform;
 
            if( xform != null )
            {
                xform.anchorMin     = new Vector2( 0.0f, 1.0f );
 
                xform.anchorMax     = new Vector2( 1.0f, 1.0f );
 
                xform.pivot         = new Vector2( 0.5f, 1.0f );
 
                xform.localPosition = Vector3.zero;
 
                xform.offsetMin     = Vector2.zero;
 
                xform.offsetMax     = Vector2.zero;
 
                xform.sizeDelta     = new Vector2( xform.sizeDelta.x, 20.0f );
            }
 
 
            xform = lbl.transform as RectTransform;
 
            if( xform != null )
            {
                xform.anchorMin     = new Vector2( 0.0f, 0.0f );
 
                xform.anchorMax     = new Vector2( 1.0f, 1.0f );
 
                xform.pivot         = new Vector2( 0.5f, 0.5f );
 
                xform.localPosition = Vector3.zero;
 
                xform.offsetMin     = Vector2.zero;
 
                xform.offsetMax     = Vector2.zero;
            }
 
            if( txt != null )
            {
                txt.font      = Resources.GetBuiltinResource< Font >( "Arial.ttf" );
 
                txt.alignment = TextAnchor.MiddleCenter;
 
                txt.color     = Color.black;
 
                txt.text      = "List Item";
            }
 
            if( itm != null ) 
            {
                itm.img   = img;
 
                itm.label = txt;
            }
 
            template = tpl.transform as RectTransform;
        }
 
        return template;
    }
 
    //************************************************************************************
    //
    //************************************************************************************
 
    [ Serializable ] public sealed class ScrollListItem : MonoBehaviour, IPointerClickHandler, ScrollList.IItemData
    {
        static public object            selection { get; set; }
 
        static public readonly Color[]  colors = new Color[ 3 ] { Color.white, Color.grey, Color.yellow };
 
 
        [ SerializeField ] public  RawImage   img   = null;
 
        [ SerializeField ] public  Text       label = null;
 
        [ NonSerialized  ] private int        id    = 0;
 
        [ NonSerialized  ] private object     data  = null;
 
        [ NonSerialized  ] private ScrollList lst   = null;
 
        public void UpdateColor   ()                       { if( img != null ) { img.color = ( selection != data ) || ( selection == null ) ? colors[ id & 1 ] : colors[ 2 ]; } }
 
        public void OnPointerClick( PointerEventData evt ) { selection = data; if( lst != null ) lst.dirty = true; }
 
        public void OnTransformParentChanged() { if( transform.parent.gameObject.activeInHierarchy == false ) { data = null; } }
 
        public void Bind( ScrollList list, int item, object obj ) 
        { 
            lst  = list; 
 
            id   = item; 
 
            data = obj; 
 
            if( label != null ) label.text = item.ToString(); 
 
            UpdateColor(); 
        }
    }
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox