ScrollList

From Unify Community Wiki
Revision as of 18:01, 6 July 2020 by Serge Billault (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Author: Serge Billault

Contents

Archive

ScrollList.zip

Archive Content

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

Modes supported

. Pooled
. Non pooled ( Dual Ranges )

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 mode only.
. Networking: None.

Description

. Non destructive 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).


Scroll list pic 0.jpg

Usage

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


Scroll list pic 1.jpg

Code

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 replacedType = replaced.GetType();
 
            string  json = JsonUtility.ToJson( replaced );
 
            DestroyImmediate( replaced );
 
 
            ScrollRect replacing = ( replacedType == typeof( ScrollRect ) ) ? trg.AddComponent< ScrollList >() : trg.AddComponent< ScrollRect >();
 
            if( replacing != null )
            {
                JsonUtility.FromJsonOverwrite( json, replacing );
            }
        }
    }
 
#endif
 
    //************************************************************************************
    //
    //************************************************************************************
 
    [ SerializeField ] public  bool                       m_use_pool    = false;
 
    [ SerializeField ] public  RectTransform              m_template    = null;
 
    [ NonSerialized  ] private bool                       m_pooled      = false;
 
    [ NonSerialized  ] private Scrollbar                  m_scroll_bar  = null;
 
    [ 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.CompareOrdinal( child.name, name ) == 0 ) 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_bar  = verticalScrollbar;
 
        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( ( m_scroll_bar  != null ) ? 1.0f - m_scroll_bar.value : 0.0f );
 
        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( Range range )
    {
        if( m_range.Equals( range ) == false )
        {
            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( 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 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