DebugLogRecycledListView.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. using UnityEngine.UI;
  4. // Handles the log items in an optimized way such that existing log items are
  5. // recycled within the list instead of creating a new log item at each chance
  6. namespace IngameDebugConsole
  7. {
  8. public class DebugLogRecycledListView : MonoBehaviour
  9. {
  10. #pragma warning disable 0649
  11. // Cached components
  12. [SerializeField]
  13. private RectTransform transformComponent;
  14. [SerializeField]
  15. private RectTransform viewportTransform;
  16. [SerializeField]
  17. private DebugLogManager debugManager;
  18. [SerializeField]
  19. private Color logItemNormalColor1;
  20. [SerializeField]
  21. private Color logItemNormalColor2;
  22. [SerializeField]
  23. private Color logItemSelectedColor;
  24. #pragma warning restore 0649
  25. private DebugLogManager manager;
  26. private ScrollRect scrollView;
  27. private float logItemHeight, _1OverLogItemHeight;
  28. private float viewportHeight;
  29. // Unique debug entries
  30. private List<DebugLogEntry> collapsedLogEntries = null;
  31. // Indices of debug entries to show in collapsedLogEntries
  32. private DebugLogIndexList indicesOfEntriesToShow = null;
  33. private int indexOfSelectedLogEntry = int.MaxValue;
  34. private float positionOfSelectedLogEntry = float.MaxValue;
  35. private float heightOfSelectedLogEntry;
  36. private float deltaHeightOfSelectedLogEntry;
  37. // Log items used to visualize the debug entries at specified indices
  38. private Dictionary<int, DebugLogItem> logItemsAtIndices = new Dictionary<int, DebugLogItem>();
  39. private bool isCollapseOn = false;
  40. // Current indices of debug entries shown on screen
  41. private int currentTopIndex = -1, currentBottomIndex = -1;
  42. public float ItemHeight { get { return logItemHeight; } }
  43. public float SelectedItemHeight { get { return heightOfSelectedLogEntry; } }
  44. private void Awake()
  45. {
  46. scrollView = viewportTransform.GetComponentInParent<ScrollRect>();
  47. scrollView.onValueChanged.AddListener( ( pos ) => UpdateItemsInTheList( false ) );
  48. viewportHeight = viewportTransform.rect.height;
  49. }
  50. public void Initialize( DebugLogManager manager, List<DebugLogEntry> collapsedLogEntries, DebugLogIndexList indicesOfEntriesToShow, float logItemHeight )
  51. {
  52. this.manager = manager;
  53. this.collapsedLogEntries = collapsedLogEntries;
  54. this.indicesOfEntriesToShow = indicesOfEntriesToShow;
  55. this.logItemHeight = logItemHeight;
  56. _1OverLogItemHeight = 1f / logItemHeight;
  57. }
  58. public void SetCollapseMode( bool collapse )
  59. {
  60. isCollapseOn = collapse;
  61. }
  62. // A log item is clicked, highlight it
  63. public void OnLogItemClicked( DebugLogItem item )
  64. {
  65. OnLogItemClickedInternal( item.Index, item );
  66. }
  67. // Force expand the log item at specified index
  68. public void SelectAndFocusOnLogItemAtIndex( int itemIndex )
  69. {
  70. if( indexOfSelectedLogEntry != itemIndex ) // Make sure that we aren't deselecting the target log item
  71. OnLogItemClickedInternal( itemIndex );
  72. float transformComponentCenterYAtTop = viewportHeight * 0.5f;
  73. float transformComponentCenterYAtBottom = transformComponent.sizeDelta.y - viewportHeight * 0.5f;
  74. float transformComponentTargetCenterY = itemIndex * logItemHeight + viewportHeight * 0.5f;
  75. if( transformComponentCenterYAtTop == transformComponentCenterYAtBottom )
  76. scrollView.verticalNormalizedPosition = 0.5f;
  77. else
  78. scrollView.verticalNormalizedPosition = Mathf.Clamp01( Mathf.InverseLerp( transformComponentCenterYAtBottom, transformComponentCenterYAtTop, transformComponentTargetCenterY ) );
  79. manager.SetSnapToBottom( false );
  80. }
  81. private void OnLogItemClickedInternal( int itemIndex, DebugLogItem referenceItem = null )
  82. {
  83. if( indexOfSelectedLogEntry != itemIndex )
  84. {
  85. DeselectSelectedLogItem();
  86. if( !referenceItem )
  87. {
  88. if( currentTopIndex == -1 )
  89. UpdateItemsInTheList( false ); // Try to generate some DebugLogItems, we need one DebugLogItem to calculate the text height
  90. referenceItem = logItemsAtIndices[currentTopIndex];
  91. }
  92. indexOfSelectedLogEntry = itemIndex;
  93. positionOfSelectedLogEntry = itemIndex * logItemHeight;
  94. heightOfSelectedLogEntry = referenceItem.CalculateExpandedHeight( collapsedLogEntries[indicesOfEntriesToShow[itemIndex]].ToString() );
  95. deltaHeightOfSelectedLogEntry = heightOfSelectedLogEntry - logItemHeight;
  96. manager.SetSnapToBottom( false );
  97. }
  98. else
  99. DeselectSelectedLogItem();
  100. if( indexOfSelectedLogEntry >= currentTopIndex && indexOfSelectedLogEntry <= currentBottomIndex )
  101. ColorLogItem( logItemsAtIndices[indexOfSelectedLogEntry], indexOfSelectedLogEntry );
  102. CalculateContentHeight();
  103. HardResetItems();
  104. UpdateItemsInTheList( true );
  105. manager.ValidateScrollPosition();
  106. }
  107. // Deselect the currently selected log item
  108. public void DeselectSelectedLogItem()
  109. {
  110. int indexOfPreviouslySelectedLogEntry = indexOfSelectedLogEntry;
  111. indexOfSelectedLogEntry = int.MaxValue;
  112. positionOfSelectedLogEntry = float.MaxValue;
  113. heightOfSelectedLogEntry = deltaHeightOfSelectedLogEntry = 0f;
  114. if( indexOfPreviouslySelectedLogEntry >= currentTopIndex && indexOfPreviouslySelectedLogEntry <= currentBottomIndex )
  115. ColorLogItem( logItemsAtIndices[indexOfPreviouslySelectedLogEntry], indexOfPreviouslySelectedLogEntry );
  116. }
  117. // Number of debug entries may be changed, update the list
  118. public void OnLogEntriesUpdated( bool updateAllVisibleItemContents )
  119. {
  120. CalculateContentHeight();
  121. viewportHeight = viewportTransform.rect.height;
  122. if( updateAllVisibleItemContents )
  123. HardResetItems();
  124. UpdateItemsInTheList( updateAllVisibleItemContents );
  125. }
  126. // A single collapsed log entry at specified index is updated, refresh its item if visible
  127. public void OnCollapsedLogEntryAtIndexUpdated( int index )
  128. {
  129. DebugLogItem logItem;
  130. if( logItemsAtIndices.TryGetValue( index, out logItem ) )
  131. logItem.ShowCount();
  132. }
  133. // Log window is resized, update the list
  134. public void OnViewportDimensionsChanged()
  135. {
  136. viewportHeight = viewportTransform.rect.height;
  137. UpdateItemsInTheList( false );
  138. }
  139. private void HardResetItems()
  140. {
  141. if( currentTopIndex != -1 )
  142. {
  143. DestroyLogItemsBetweenIndices( currentTopIndex, currentBottomIndex );
  144. currentTopIndex = -1;
  145. }
  146. }
  147. private void CalculateContentHeight()
  148. {
  149. float newHeight = Mathf.Max( 1f, indicesOfEntriesToShow.Count * logItemHeight + deltaHeightOfSelectedLogEntry );
  150. transformComponent.sizeDelta = new Vector2( 0f, newHeight );
  151. }
  152. // Calculate the indices of log entries to show
  153. // and handle log items accordingly
  154. public void UpdateItemsInTheList( bool updateAllVisibleItemContents )
  155. {
  156. // If there is at least one log entry to show
  157. if( indicesOfEntriesToShow.Count > 0 )
  158. {
  159. float contentPosTop = transformComponent.anchoredPosition.y - 1f;
  160. float contentPosBottom = contentPosTop + viewportHeight + 2f;
  161. if( positionOfSelectedLogEntry <= contentPosBottom )
  162. {
  163. if( positionOfSelectedLogEntry <= contentPosTop )
  164. {
  165. contentPosTop -= deltaHeightOfSelectedLogEntry;
  166. contentPosBottom -= deltaHeightOfSelectedLogEntry;
  167. if( contentPosTop < positionOfSelectedLogEntry - 1f )
  168. contentPosTop = positionOfSelectedLogEntry - 1f;
  169. if( contentPosBottom < contentPosTop + 2f )
  170. contentPosBottom = contentPosTop + 2f;
  171. }
  172. else
  173. {
  174. contentPosBottom -= deltaHeightOfSelectedLogEntry;
  175. if( contentPosBottom < positionOfSelectedLogEntry + 1f )
  176. contentPosBottom = positionOfSelectedLogEntry + 1f;
  177. }
  178. }
  179. int newTopIndex = (int) ( contentPosTop * _1OverLogItemHeight );
  180. int newBottomIndex = (int) ( contentPosBottom * _1OverLogItemHeight );
  181. if( newTopIndex < 0 )
  182. newTopIndex = 0;
  183. if( newBottomIndex > indicesOfEntriesToShow.Count - 1 )
  184. newBottomIndex = indicesOfEntriesToShow.Count - 1;
  185. if( currentTopIndex == -1 )
  186. {
  187. // There are no log items visible on screen,
  188. // just create the new log items
  189. updateAllVisibleItemContents = true;
  190. currentTopIndex = newTopIndex;
  191. currentBottomIndex = newBottomIndex;
  192. CreateLogItemsBetweenIndices( newTopIndex, newBottomIndex );
  193. }
  194. else
  195. {
  196. // There are some log items visible on screen
  197. if( newBottomIndex < currentTopIndex || newTopIndex > currentBottomIndex )
  198. {
  199. // If user scrolled a lot such that, none of the log items are now within
  200. // the bounds of the scroll view, pool all the previous log items and create
  201. // new log items for the new list of visible debug entries
  202. updateAllVisibleItemContents = true;
  203. DestroyLogItemsBetweenIndices( currentTopIndex, currentBottomIndex );
  204. CreateLogItemsBetweenIndices( newTopIndex, newBottomIndex );
  205. }
  206. else
  207. {
  208. // User did not scroll a lot such that, there are still some log items within
  209. // the bounds of the scroll view. Don't destroy them but update their content,
  210. // if necessary
  211. if( newTopIndex > currentTopIndex )
  212. DestroyLogItemsBetweenIndices( currentTopIndex, newTopIndex - 1 );
  213. if( newBottomIndex < currentBottomIndex )
  214. DestroyLogItemsBetweenIndices( newBottomIndex + 1, currentBottomIndex );
  215. if( newTopIndex < currentTopIndex )
  216. {
  217. CreateLogItemsBetweenIndices( newTopIndex, currentTopIndex - 1 );
  218. // If it is not necessary to update all the log items,
  219. // then just update the newly created log items. Otherwise,
  220. // wait for the major update
  221. if( !updateAllVisibleItemContents )
  222. UpdateLogItemContentsBetweenIndices( newTopIndex, currentTopIndex - 1 );
  223. }
  224. if( newBottomIndex > currentBottomIndex )
  225. {
  226. CreateLogItemsBetweenIndices( currentBottomIndex + 1, newBottomIndex );
  227. // If it is not necessary to update all the log items,
  228. // then just update the newly created log items. Otherwise,
  229. // wait for the major update
  230. if( !updateAllVisibleItemContents )
  231. UpdateLogItemContentsBetweenIndices( currentBottomIndex + 1, newBottomIndex );
  232. }
  233. }
  234. currentTopIndex = newTopIndex;
  235. currentBottomIndex = newBottomIndex;
  236. }
  237. if( updateAllVisibleItemContents )
  238. {
  239. // Update all the log items
  240. UpdateLogItemContentsBetweenIndices( currentTopIndex, currentBottomIndex );
  241. }
  242. }
  243. else
  244. HardResetItems();
  245. }
  246. private void CreateLogItemsBetweenIndices( int topIndex, int bottomIndex )
  247. {
  248. for( int i = topIndex; i <= bottomIndex; i++ )
  249. CreateLogItemAtIndex( i );
  250. }
  251. // Create (or unpool) a log item
  252. private void CreateLogItemAtIndex( int index )
  253. {
  254. DebugLogItem logItem = debugManager.PopLogItem();
  255. // Reposition the log item
  256. Vector2 anchoredPosition = new Vector2( 1f, -index * logItemHeight );
  257. if( index > indexOfSelectedLogEntry )
  258. anchoredPosition.y -= deltaHeightOfSelectedLogEntry;
  259. logItem.Transform.anchoredPosition = anchoredPosition;
  260. // Color the log item
  261. ColorLogItem( logItem, index );
  262. // To access this log item easily in the future, add it to the dictionary
  263. logItemsAtIndices[index] = logItem;
  264. }
  265. private void DestroyLogItemsBetweenIndices( int topIndex, int bottomIndex )
  266. {
  267. for( int i = topIndex; i <= bottomIndex; i++ )
  268. debugManager.PoolLogItem( logItemsAtIndices[i] );
  269. }
  270. private void UpdateLogItemContentsBetweenIndices( int topIndex, int bottomIndex )
  271. {
  272. DebugLogItem logItem;
  273. for( int i = topIndex; i <= bottomIndex; i++ )
  274. {
  275. logItem = logItemsAtIndices[i];
  276. logItem.SetContent( collapsedLogEntries[indicesOfEntriesToShow[i]], i, i == indexOfSelectedLogEntry );
  277. if( isCollapseOn )
  278. logItem.ShowCount();
  279. else
  280. logItem.HideCount();
  281. }
  282. }
  283. // Color a log item using its index
  284. private void ColorLogItem( DebugLogItem logItem, int index )
  285. {
  286. if( index == indexOfSelectedLogEntry )
  287. logItem.Image.color = logItemSelectedColor;
  288. else if( index % 2 == 0 )
  289. logItem.Image.color = logItemNormalColor1;
  290. else
  291. logItem.Image.color = logItemNormalColor2;
  292. }
  293. }
  294. }