Virtual Scroll is a high-performance virtualized list for Unity uGUI. It materializes only the items near the viewport and supports both fixed-size and variable-size content.
- Vertical and horizontal lists.
- O(1) visible-index lookup for fixed-size items.
- O(log N) lookup and size updates for variable-size items using a Fenwick tree.
- Fixed-size multi-lane grids and variable-size masonry layouts.
- Automatic runtime capture for
VerticalLayoutGroup,HorizontalLayoutGroup, andGridLayoutGroup. - Independent uniform main-axis and cross-axis spacing.
- Typed item pools for lists with multiple visual templates.
- Runtime item creation through a small data-source interface.
- No LINQ or per-scroll-frame collection allocation in the virtualization path.
- Reset, numeric-offset, data-anchor, and end-pinned refresh strategies.
- Incremental insertion, removal, move, range refresh, scroll-to-index, and measured-size update APIs.
- Optional insertion and removal animations using unscaled time.
- Scroll-anchor preservation when a variable item changes size.
- Unity 2021.3 or newer.
- uGUI (
com.unity.ugui).
Open Window > Package Manager, select Add package from git URL, and enter:
https://github.com/TristinOrg/VirtualScroll.git
You can pin a release tag:
https://github.com/TristinOrg/VirtualScroll.git#v1.1.0
The current main branch uses the TristinOrg.VirtualScroll namespace and the com.tristinorg.virtual-scroll package identifier. Version 1.1.0 remains available under the former namespace for existing projects. When upgrading from 1.x, replace TristinWen.VirtualScroll with TristinOrg.VirtualScroll and update any manifest dependency key to com.tristinorg.virtual-scroll.
- Create a normal uGUI
Scroll View. - Replace its
ScrollRectcomponent withVirtualScrollView. - Keep the content reference assigned. Viewport may be assigned explicitly or left empty to use the component RectTransform.
- A supported
LayoutGroupmay remain on the content for familiar authoring. Its parameters are captured and the component is disabled at runtime. A matchingContentSizeFitteris also disabled while virtual layout owns content size. - Add an
IVirtualScrollItemcomponent to the item prefab, implementIVirtualScrollDataSource, and callSetDataSourceafter your data is ready.
Initialization can explicitly select its starting position behavior:
ScrollView.SetDataSource(dataSource, EVirtualScrollPositionMode.Reset);
ScrollView.SetDataSource(dataSource, EVirtualScrollPositionMode.KeepOffset);
ScrollView.SetDataSource(dataSource, EVirtualScrollPositionMode.KeepAnchor);
ScrollView.SetDataSource(dataSource, EVirtualScrollPositionMode.StickToEnd);using TristinOrg.VirtualScroll;
using UnityEngine;
public sealed class MailListItem : MonoBehaviour, IVirtualScrollItem
{
public RectTransform Transform => transform as RectTransform;
public void SetIndex(int index)
{
name = $"Mail {index}";
}
public void Clear()
{
name = "Pooled Mail";
}
}
public sealed class MailListPresenter : MonoBehaviour, IVirtualScrollDataSource
{
public VirtualScrollView ScrollView;
public MailListItem ItemPrefab;
public int Count => 10000;
private void Start()
{
ScrollView.SizeMode = EVirtualScrollSizeMode.Variable;
ScrollView.Spacing = 8f;
ScrollView.SetDataSource(this);
}
public int GetItemType(int index)
{
return 0;
}
public float GetItemSize(int index)
{
return 72f + index % 4 * 24f;
}
public IVirtualScrollItem CreateItem(int itemType, Transform parent)
{
return Instantiate(ItemPrefab, parent);
}
public void BindItem(IVirtualScrollItem item, int index)
{
((MailListItem)item).SetIndex(index);
}
public void UnbindItem(IVirtualScrollItem item, int index)
{
((MailListItem)item).Clear();
}
}CreateItem returns the component implementing IVirtualScrollItem, not its RectTransform. VirtualScrollView caches both the interface and its Transform when the item is materialized. BindItem and UnbindItem can therefore cast directly to the known view type without calling GetComponent on every reuse. The same interface instance is retained in the typed pool and supplied again on the next bind.
IVirtualScrollItem.Transform must return the root RectTransform that the virtual scroll view is allowed to parent, position, size, activate, and recycle. Keep the returned transform stable for the lifetime of the item.
Set EstimatedMainAxisSize to a representative item height or width. Variable layouts build their initial offset index from this estimate without calling GetItemSize for the entire data set. GetItemSize(index) is requested once when an item approaches the viewport, and the total content size is calibrated incrementally.
FixedMainAxisSize means height for vertical scrolling and width for horizontal scrolling. The former FixedItemSize API remains as an obsolete source-compatible alias, and existing serialized values migrate automatically.
When text or asynchronous content determines the final height:
- Configure a stable
EstimatedMainAxisSize. - Bind and lay out the visible item.
- Measure its final height.
- Call
NotifyItemSizeChanged(index, measuredHeight)only when the value actually changes.
The size index updates in O(log N), and the list preserves the first visible item's viewport position.
ScrollView.ReloadData();
ScrollView.ReloadData(EVirtualScrollPositionMode.Reset);
ScrollView.ReloadData(EVirtualScrollPositionMode.KeepAnchor);
ScrollView.ReloadData(EVirtualScrollPositionMode.StickToEnd);
ScrollView.RefreshItem(index);
ScrollView.RefreshRange(startIndex, count);
ScrollView.NotifyItemSizeChanged(index, newSize);
ScrollView.ScrollToIndex(index, EVirtualScrollAlignment.Start); // Item starts at the viewport leading edge.
ScrollView.ScrollToIndex(index, EVirtualScrollAlignment.Center); // Item is centered in the viewport.
ScrollView.ScrollToIndex(index, EVirtualScrollAlignment.End); // Item ends at the viewport trailing edge.ScrollToIndex clamps the requested index and content offset to valid bounds. Near the beginning or end of the list, the exact visual alignment may therefore be limited by the available scroll range.
The legacy convenience overload remains available:
ScrollView.ReloadData(true); // Keep the numeric content offset.
ScrollView.ReloadData(false); // Reset to the beginning.Mutate your backing collection first, then notify the scroll view with the same indices and counts:
Mails.InsertRange(index, incomingMails);
ScrollView.NotifyItemsInserted(index, incomingMails.Count);
Mails.RemoveRange(index, count);
ScrollView.NotifyItemsRemoved(index, count);
var movedMail = Mails[oldIndex];
Mails.RemoveAt(oldIndex);
Mails.Insert(newIndex, movedMail);
ScrollView.NotifyItemMoved(oldIndex, newIndex);Visible views representing unchanged logical items are remapped and retained. KeepAnchor is the default for collection changes, so inserting older mail above the viewport does not move the reader's current mail.
Set AnimateChanges to enable animations for visible insertions, removals, and moved-item entrances. Leave AnimationProvider empty to use the built-in unscaled-time opacity and scale animation.
Animations are attached to collection notifications, not BindItem. Always perform operations in this order:
- Change the backing collection or count.
- Call the matching
NotifyItemsInserted,NotifyItemsRemoved, orNotifyItemMovedmethod. - Let
VirtualScrollViewanimate only the affected views that are currently materialized.
Normal scrolling and ordinary item reuse do not start collection animations.
Import Runtime List Example from Package Manager, then configure a GameObject as follows:
- Add
RuntimeListExampleto the same GameObject that should own the sample animation. - Assign the scene's
VirtualScrollViewtoRuntimeListExample.ScrollView. - Leave both provider fields empty to let the example add its default
SlideListAnimation.VirtualScrollView.AnimationProviderhas priority when configured;RuntimeListExample.AnimationProvideris only a fallback and cannot overwrite the ScrollView field. - Enter Play Mode.
- Open the
RuntimeListExamplecomponent context menu and select Insert Visible Item, Remove Visible Item, or Scroll To Item. SetTargetIndexandTargetAlignmentbefore using the positioning action. The same public methods can be connected directly to uGUI ButtonOnClickevents.
The default provider is in Samples~/RuntimeListExample/SlideListAnimation.cs. Insertions move from right to left while fading from transparent to opaque and scaling from CollapsedScale to the resting scale. Removals perform the inverse presentation toward the left. The example applies its AnimationDuration value (0.6 seconds by default) to make playback easy to observe. It supports concurrent items, uses no coroutines, clamps each unscaled-time step to Time.maximumDeltaTime, and restores position, scale, and opacity before pooled reuse.
RuntimeListExample.AnimationProvider accepts any MonoBehaviour. The assigned component must implement IVirtualScrollAnimation, so the same field can be used with the included sample, PrimeTween, DOTween, Animator, or a project-specific provider.
The example insertion and removal methods deliberately change ItemCount before notifying the scroll view:
public void InsertVisibleItem()
{
var index = Mathf.Max(0, ScrollView.FirstViewportIndex);
ItemCount++;
ScrollView.NotifyItemsInserted(index, 1, EVirtualScrollPositionMode.KeepOffset);
}
public void RemoveVisibleItem()
{
var index = Mathf.Clamp(ScrollView.FirstViewportIndex, 0, ItemCount - 1);
ItemCount--;
ScrollView.NotifyItemsRemoved(index, 1, EVirtualScrollPositionMode.KeepOffset);
}
public void ScrollToItem()
{
var index = Mathf.Clamp(TargetIndex, 0, ItemCount - 1);
ScrollView.ScrollToIndex(index, TargetAlignment);
}KeepOffset makes the changed visible position easy to observe in the sample. Production code can keep the default KeepAnchor, use StickToEnd for chat messages, or choose another position mode independently of animation.
FirstViewportIndex excludes overscan and is appropriate for actions that must be visibly demonstrated. FirstMaterializedIndex and LastMaterializedIndex include overscan and are intended for virtualization diagnostics. The legacy FirstVisibleIndex and LastVisibleIndex properties retain their materialized semantics for compatibility.
Implement IVirtualScrollAnimation when a project wants DOTween, PrimeTween, Animator, or its own update system. Assign the implementing MonoBehaviour to VirtualScrollView.AnimationProvider in the Inspector:
If a configured component does not implement IVirtualScrollAnimation, the scroll view logs an error and skips that collection animation. It does not silently substitute the built-in scale-and-fade presentation. Leave the provider empty explicitly when the built-in animation is desired.
Before Play is called, VirtualScrollView has already applied the item's resting layout. While Play owns the presentation, the current visibility refresh does not overwrite the provider's insertion position. Removed views retain their existing binding, remain materialized, and render above replacement views until context.Complete() is called. Retained items keep their previous positions during removal playback and move into the released space only after the final visible removal completes. UnbindItem runs only after the removal animation completes.
public sealed class CustomListAnimation : MonoBehaviour, IVirtualScrollAnimation
{
public void Play(VirtualScrollAnimationContext context)
{
// Start playback for context.Item.
// Use context.AnimationType to select insert or remove presentation.
// Use context.Duration when the scroll-view duration should be respected.
// Call exactly once after natural completion.
context.Complete();
}
public void Cancel(VirtualScrollAnimationContext context)
{
// Stop the animation identified by context.AnimationId.
// Restore every changed property immediately.
// Do not call context.Complete() from this method.
}
}Code-created providers that are not MonoBehaviour instances can instead use the runtime-only property:
ScrollView.Animation = customAnimation;
ScrollView.AnimateChanges = true;VirtualScrollAnimationContext provides:
Item: the materializedRectTransformbeing animated.AnimationType:InsertorRemove.Duration:ChangeAnimationDuration, clamped to a positive value.AnimationId: a unique identifier for matching concurrent playback and cancellation.Complete(): signals natural completion. Removed views are returned to the pool only after this call.
Playowns timing and presentation.VirtualScrollViewdoes not start a coroutine for a custom provider.Cancelmay run when an item scrolls away, is rebound, another animation replaces it, or the scroll view is destroyed.Cancelmust stop external tweens and restore every modified property so the pool never retains scale, opacity, position, rotation, or material state.- Call
context.Complete()after natural completion, including when disabling a provider that still owns active animations. Otherwise removed views must remain detached and cannot return to the pool. - Do not call
Complete()fromCancel; the scroll view has already ended ownership for that animation. - Stale and duplicate
Complete()calls are ignored throughAnimationId, so a late callback cannot recycle a newly rebound item. - A provider must support multiple simultaneous contexts when a range is inserted or removed.
Animations affect currently materialized items only. Inserting outside the viewport still updates indices and layout, but creates no animation work.
Set CrossAxisCount above one:
SizeMode.Fixedcreates an equal-size grid.SizeMode.Variableassigns equal-width items to the currently shortest lane, producing a masonry layout.Spacingcontrols distance along the scrolling axis.CrossAxisSpacingcontrols distance between lanes.
With UseLayoutGroupSettings enabled (the default), VirtualScrollView captures these values from the content at initialization and then disables the source component to avoid continuous layout rebuilding:
VerticalLayoutGroup: vertical direction, padding, spacing, and alignment.HorizontalLayoutGroup: horizontal direction, padding, spacing, and alignment.GridLayoutGroup: direction, padding, spacing, cell size, constraint/count, alignment, start axis, and cross-axis start corner.
For a fixed-size grid, leave OverrideLayoutItemSize disabled to use GridLayoutGroup.cellSize. Enable it to keep FixedMainAxisSize; spacing, padding, lane count, alignment, and cross-axis cell size still come from the GridLayoutGroup.
For variable-size items such as mail content, select SizeMode.Variable. GetItemSize(index) controls each item's main-axis size while spacing and lane parameters continue to come from the captured LayoutGroup.
Call RecaptureLayoutGroup() after changing LayoutGroup parameters at runtime. Destroying VirtualScrollView restores the captured LayoutGroup and ContentSizeFitter to their original enabled states.
- Fixed-size offset lookup: O(1).
- Variable-size offset lookup: O(log N).
- Variable-size update: O(log N).
- Scroll work: proportional to items entering or leaving the viewport, not total data count.
- Created GameObjects: proportional to the largest observed visible range plus overscan.
Actual frame time depends on item binding, text generation, shaders, and Canvas topology. Profile representative UI on target hardware before setting budgets.
- Lanes use equal cross-axis widths; variable-width masonry items are not supported.
- Insert and remove animations affect currently materialized items only, by design.
- Data mutations must occur before their matching notification method is called.
MIT. See LICENSE.md.