using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace SimpleFileBrowser { [RequireComponent( typeof( ScrollRect ) )] public class RecycledListView : MonoBehaviour #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WSA || UNITY_WSA_10_0 , IPointerClickHandler #endif { #pragma warning disable 0649 #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WSA || UNITY_WSA_10_0 [SerializeField] private FileBrowser fileBrowser; #endif // Cached components [SerializeField] private RectTransform viewportTransform; [SerializeField] private RectTransform contentTransform; #pragma warning restore 0649 private float itemHeight, _1OverItemHeight; private float viewportHeight; private readonly Dictionary items = new Dictionary(); private readonly Stack pooledItems = new Stack(); IListViewAdapter adapter = null; // Current indices of items shown on screen private int currentTopIndex = -1, currentBottomIndex = -1; private void Start() { viewportHeight = viewportTransform.rect.height; GetComponent().onValueChanged.AddListener( ( pos ) => UpdateItemsInTheList() ); } public void SetAdapter( IListViewAdapter adapter ) { this.adapter = adapter; itemHeight = adapter.ItemHeight; _1OverItemHeight = 1f / itemHeight; } public void OnSkinRefreshed() { if( currentTopIndex >= 0 ) { DestroyItemsBetweenIndices( currentTopIndex, currentBottomIndex ); currentTopIndex = currentBottomIndex = -1; } itemHeight = adapter.ItemHeight; _1OverItemHeight = 1f / itemHeight; UpdateList(); } // Update the list public void UpdateList() { float newHeight = Mathf.Max( 1f, adapter.Count * itemHeight ); contentTransform.sizeDelta = new Vector2( 0f, newHeight ); viewportHeight = viewportTransform.rect.height; UpdateItemsInTheList( true ); } // Window is resized, update the list public void OnViewportDimensionsChanged() { viewportHeight = viewportTransform.rect.height; UpdateItemsInTheList(); } // Calculate the indices of items to show private void UpdateItemsInTheList( bool updateAllVisibleItems = false ) { // If there is at least one item to show if( adapter.Count > 0 ) { float contentPos = contentTransform.anchoredPosition.y - 1f; int newTopIndex = (int) ( contentPos * _1OverItemHeight ); int newBottomIndex = (int) ( ( contentPos + viewportHeight + 2f ) * _1OverItemHeight ); if( newTopIndex < 0 ) newTopIndex = 0; if( newBottomIndex > adapter.Count - 1 ) newBottomIndex = adapter.Count - 1; if( currentTopIndex == -1 ) { // There are no active items updateAllVisibleItems = true; currentTopIndex = newTopIndex; currentBottomIndex = newBottomIndex; CreateItemsBetweenIndices( newTopIndex, newBottomIndex ); } else { // There are some active items if( newBottomIndex < currentTopIndex || newTopIndex > currentBottomIndex ) { // If user scrolled a lot such that, none of the items are now within // the bounds of the scroll view, pool all the previous items and create // new items for the new list of visible entries updateAllVisibleItems = true; DestroyItemsBetweenIndices( currentTopIndex, currentBottomIndex ); CreateItemsBetweenIndices( newTopIndex, newBottomIndex ); } else { // User did not scroll a lot such that, some items are are still within // the bounds of the scroll view. Don't destroy them but update their content, // if necessary if( newTopIndex > currentTopIndex ) { DestroyItemsBetweenIndices( currentTopIndex, newTopIndex - 1 ); } if( newBottomIndex < currentBottomIndex ) { DestroyItemsBetweenIndices( newBottomIndex + 1, currentBottomIndex ); } if( newTopIndex < currentTopIndex ) { CreateItemsBetweenIndices( newTopIndex, currentTopIndex - 1 ); // If it is not necessary to update all the items, // then just update the newly created items. Otherwise, // wait for the major update if( !updateAllVisibleItems ) { UpdateItemContentsBetweenIndices( newTopIndex, currentTopIndex - 1 ); } } if( newBottomIndex > currentBottomIndex ) { CreateItemsBetweenIndices( currentBottomIndex + 1, newBottomIndex ); // If it is not necessary to update all the items, // then just update the newly created items. Otherwise, // wait for the major update if( !updateAllVisibleItems ) { UpdateItemContentsBetweenIndices( currentBottomIndex + 1, newBottomIndex ); } } } currentTopIndex = newTopIndex; currentBottomIndex = newBottomIndex; } if( updateAllVisibleItems ) { // Update all the items UpdateItemContentsBetweenIndices( currentTopIndex, currentBottomIndex ); } } else if( currentTopIndex != -1 ) { // There is nothing to show but some items are still visible; pool them DestroyItemsBetweenIndices( currentTopIndex, currentBottomIndex ); currentTopIndex = -1; } } private void CreateItemsBetweenIndices( int topIndex, int bottomIndex ) { for( int i = topIndex; i <= bottomIndex; i++ ) { CreateItemAtIndex( i ); } } // Create (or unpool) an item private void CreateItemAtIndex( int index ) { ListItem item; if( pooledItems.Count > 0 ) { item = pooledItems.Pop(); item.gameObject.SetActive( true ); } else { item = adapter.CreateItem(); item.transform.SetParent( contentTransform, false ); item.SetAdapter( adapter ); } // Reposition the item ( (RectTransform) item.transform ).anchoredPosition = new Vector2( 1f, -index * itemHeight ); // To access this item easily in the future, add it to the dictionary items[index] = item; } private void DestroyItemsBetweenIndices( int topIndex, int bottomIndex ) { for( int i = topIndex; i <= bottomIndex; i++ ) { ListItem item = items[i]; item.gameObject.SetActive( false ); pooledItems.Push( item ); } } private void UpdateItemContentsBetweenIndices( int topIndex, int bottomIndex ) { for( int i = topIndex; i <= bottomIndex; i++ ) { ListItem item = items[i]; item.Position = i; adapter.SetItemContent( item ); } } #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WSA || UNITY_WSA_10_0 // When free space inside ScrollRect is clicked: // Left click: deselect selected file(s) // Right click: show context menu public void OnPointerClick( PointerEventData eventData ) { if( eventData.button == PointerEventData.InputButton.Left ) fileBrowser.DeselectAllFiles(); else if( eventData.button == PointerEventData.InputButton.Right ) fileBrowser.OnContextMenuTriggered( eventData.position ); } #endif } }