ScrollList

From Unify Community Wiki
(Difference between revisions)
Jump to: navigation, search
m (live demo)
m (motives)
Line 14: Line 14:
  
 
Live demo: [https://geoarcmap.alwaysdata.net/MOTHER WebGL application with scroll list]
 
Live demo: [https://geoarcmap.alwaysdata.net/MOTHER WebGL application with scroll list]
 +
<br>The ScrollList was motivated by the fact that restricted platforms like WebGL offer limited memory and cpu usage in a single threaded environment.
  
 
==Compatibility==
 
==Compatibility==

Revision as of 18:19, 6 July 2020

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 )

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.

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).
. 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 screeshot below


Scroll list pic 1.jpg

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 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