Grid Layout with a header using EnhancedScroller.

In this article will discuss creating a little bit hard way of listing two types of rows/items using EnhancedScroller.

play_clicked.PNG
Grid layout with a header of each section

Creating a list of a single type of data in EnhancedScroller is too easy if you have gone through the 1st demo the EnhancedScroller Plugin provides. Changing the size and UI of the same type of data is easy and faster with EnhancedScroller as you won't need to handle the list by yourself you will just need to provide the list of the data for the EnhancedScroller and it will handle them easily. The simple layout with EnhancedScroller is created using a single data list and it looks like the picture below.

SimpleList_with_multipleSize.PNG
list with different item sizes
simpleList_with_sameSizeItems.PNG
list with same item size

But in this article will discuss creating a little bit hard way of listing two types of rows/items using EnhancedScroller. The image for this type of list is shown at the top of the image of this article. As you can see in the picture at the top of this article, we have two types of layouts in the list.

  1. One is a simple layout style item with a header only.
  2. And another item has multiple items which come under the same category represented by the header section above it.

The grid items continue until the items of the same section are available to be shown and then the next section will start with another category with the same style layout of a single header and then followed by its related items in a grid layout.

This way of representing items with header and items is done using two different lists in EnhancedScroller. One list will hold all the data (including the header) which will be displayed in the scroll list and another list will hold the references for each row of the item of the scroll list. With only theory, you may not understand properly so let's learn it with some examples as below.

Requirement

The given below requirements should be fulfilled for a better understanding of this article.

1. EnhanceScroller plugin  -> You can get it from Unity Asset Store.

2. Understanding of the EnhancedScroller simple usage. -> You can check out the official document given by the EnhancedScroller at EnhancedScroller Site or check out the introduction article [Use EnhanceScroller for listing items in unity].

3. Unity game engine.

Description with demo

Scripts

1. Data classes:

First of all, we need to create scripts for the classes which will hold the data of each item on the list. For this project, we need 3 types of data classes. They are as follows:

1. Data: Base class which will be used for holding the list of items and will be used be extended class for other data classes such as header or footer.

/// Base class for items of the list
public class Data
{

}
Base data class format

2. HeaderData: Extension class of the base data that will hold the data for header items.

/// Extended class from the base itemData representing the header data
public class HeaderData : ItemData
{
    public string category;
}
Header data class format

2. BodyData: Extension class of the base data that will represent the row section for grid items. This class will have 3 variables as shown in the picture below that manage the information for the multiple item data it will hold.

/// Class holding the data for the row which will consists of multiple items
public class BodyData : Data
{
    /// indicates if the reference index  from certain list/array index set in this data
    public bool dataRefSet;

    /// starting index of the data of certain list
    public int startingDataIndex;

    /// starting index of other items
    public int endingDataIndex;
}
BodyData class format

2. ItemData: Extension class of the base data that will hold the data for the item of the list.

/// Data class for holding the data of each cell item of the grid
[Serializable]
public class ItemData : Data
{
    public string id;
    public string Text;
}
ItemData class format for each item
2. EnhancedScroller controlling script

After the data classes are created we need a script to handle them using EnahancedScroller for our desired list view layout. Let's create a new script and name it CellViewListController. This script will have an interface of EnhancedScroller IEnhancedScrollerDelegate as all the EnhancedScroller controlling scripts must-have.

public class CellViewListController : MonoBehaviour, IEnhancedScrollerDelegate
{
      #region Lists

        /// <summary>
        /// Data reference enhance scroller rely to get and update the data required
        /// </summary>
        private SmallList<Data> _dataList;

        /// <summary>
        /// Reference list of the items used for deciding which row prefab to show for the given data index by enhance scroller.
        /// if header load the appropriate header data else load the appropriate shopItems to the row representing item
        /// </summary>
        private SmallList<BodyData> _dataListRefHeaderAndRow;

        #endregion

        #region Drag and drop references

        public EnhancedScroller scroller;
        public EnhancedScrollerCellView HeaderRowViewPrefab;
        public EnhancedScrollerCellView BodyRowViewPrefab;
        public EnhancedScrollerCellView itemPrefab;

        #endregion

        #region public variables to be set from editor

        public float rowSize;
        public float headerSize;
        public int numberOfCellsPerRow = 3; //this is handled from custom editor script

        #endregion

        #region Private variables used only in this script

        /// <summary>
        /// index of the data in the full data list.
        /// It increase in count when the data of its starting and ending index value is set in the base value of `ItemDto`
        /// </summary>
        private int nextStartingIndex = 0;

        #endregion


        /// <summary>
        /// Represent the initialized condition of the enhance scroller.
        /// </summary>
        private bool _initialized;
}
EnhancedScroller controlling script CellViewListController

This CellViewListController in the above picture has many variables and they are:

  • _dataList -> Small list (provided list variant by EnhancedScroller) will be used to hold the data of the items that will be used to load the information to the list items.
  • _dataListRefHeaderAndRow ->Small list (provided list variant by EnhancedScroller) will hold the reference of the header and row(holding multiple items) data of the list. Its total count will be used for deciding the total number of rows the EnhancedScoller will control. We are using this reference list because the actual data holding list _dataList may have a dynamic number of items after header data. So to show the correct data for each row(holding multiple items) we have to use this reference data list.
  • scroller -> Reference of the EnhancedScroller which will be dragged and dropped from the editor. It is the scroller we will delegate the controller script CellViewListController for.
  • HeaderRowViewPrefab -> Prefab with an attached script of an extended class EnhancedCellView. It represents the prefab for the header row of the list.
  • BodyRowViewPrefab ->Prefab with an attached script of an extended class EnhancedCellView. It represents the prefab of a single row of the list with multiple items.
  • itemPrefab -> Prefab for each item on the list.

[EnhancedScrollerCellView must to included as an extension class for the scripts that will be used as a prefab for the item of the list. The reason is that EnhancedScroller uses EnhancedScrollerCellView for handling the items of the list in the scene, not the script we create.]

This script must be giving errors as we haven't included 3 mandatory methods for the interface class IEnhancedScrollerDelegate. Let's add them.

1. Method for providing the total number of items the list of the EnhanceScroller will handle. The total items to be shown will be the same number as per our _dataListRefHeaderAndRow of SmallList as it will hold the references of the header and row we will need in total to show all the data we have loaded in the list _dataList.

/// <summary>
/// This tells the scroller the number of cells that should have room allocated. This should be the length of your data array.
/// </summary>
/// <param name="scroller">The scroller that is requesting the data size</param>
/// <returns>The number of cells</returns>
public int GetNumberOfCells(EnhancedScroller scroller)
{
    return _dataListRefHeaderAndRow.Count;
}
Method to give the totalNumber of items to be shown in the list

2. Method for providing the size(generally height) of each item of the corresponding index from the list. The EnhancedScroller ask for the size of the item while providing the index of item from the list. So if the items are all of the same sizes then provide a size of an item else provide the size of the corresponding item's size as per the given index by the EnhancedScroller. In our case, we will provide the individual size as shown in the picture below.

/// <summary>
/// Give size(height) for the item(row of the list) corresponding to the item in the list with given dataIndex in the parameter
/// </summary>
/// <param name="scroller">EnhancedScroller controlling the list</param>
/// <param name="dataIndex">DataIndex of the row item of the list</param>
/// <returns></returns>
public float GetCellViewSize(EnhancedScroller scroller, int dataIndex)
{
    return _dataListRefHeaderAndRow[dataIndex] switch
    {
        HeaderData _ => headerSize,
        BodyData _ => rowSize,
        _ => 100
    };
}
Method to give the item size for the item on the list

3. Method to provide prefab for each data. The EnhancedScroller ask for the prefab to be used in the list by providing the dataIndex which usually correspond with the data index of the list holding actual data instead of reference data. But in our case, the provided dataIndex in the parameter will correspond to the item of the reference list _dataListRefHeaderAndRow. It is because we have given the total number of the reference list _dataListRefHeaderAndRow when asked by EnhancedScroller for a total number of rows for our list in GetNumberOfCells method above. So we will provide the prefab as per the corresponding data of the references list _dataListRefHeaderAndRow.

/// <summary>
/// Gets the cell to be displayed. You can have numerous cell types, allowing variety in your list.
/// Some examples of this would be headers, footers, and other grouping cells.
/// </summary>
/// <param name="scroller">The scroller requesting the cell</param>
/// <param name="dataIndex">The index of the data that the scroller is requesting</param>
/// <param name="cellIndex">The index of the list. This will likely be different from the dataIndex if the scroller is looping</param>
/// <returns>The cell for the scroller to use</returns>
public EnhancedScrollerCellView GetCellView(EnhancedScroller scroller, int dataIndex, int cellIndex)
{
    RowSectionView rowSectionViewInUse;
    //starting index of the shopItem data to be shown for the current item.
    if (nextStartingIndex >= _dataList.Count)
    {
        nextStartingIndex = 0;
    }

    Debug.Log($"current row index  is : {dataIndex} && dataIndex is : {nextStartingIndex}");

    //find and load the suitable prefabs of header and row (crete new or load old one)
    if (_dataListRefHeaderAndRow[dataIndex] is HeaderData)
    {
        // get a row cell prefab from the scroller, recycling old cells if possible
        rowSectionViewInUse = scroller.GetCellView(HeaderRowViewPrefab) as HeaderRowView;
        ((HeaderRowView) rowSectionViewInUse).SetData(ref _dataListRefHeaderAndRow, dataIndex, ref _dataList,
            ref nextStartingIndex, CellViewSelected);
    }
    else
    {
        // get a row cell prefab from the scroller, recycling old cells if possible
        rowSectionViewInUse = scroller.GetCellView(BodyRowViewPrefab) as BodyRowView;

        ((BodyRowView) rowSectionViewInUse).SetData(ref _dataListRefHeaderAndRow, dataIndex, ref _dataList,
            ref nextStartingIndex, CellViewSelected);
    }


    // return the cellView to the scroller
    return rowSectionViewInUse;
}
Method to give the prefab for the item of the respective index of the item in the list

As you can see from the picture above we are checking for the type of the data of reference list corresponding with the provided dataIndex in parameter. We can provide any prefab with the EnhancedScrollerCellView script attached to them but be careful we need to provide the appropriate prefab for each data type of the list row because we will need to get the proper item as we will update them after casting to the required class as given in line number 24 and 31 in the picture above.

This method will give you an error in lines 24 and 31 as there is the class name HeaderRowView and BodyRowView which we haven't created yet. They are the scripts for header and body (row with multiple items) items. They are responsible for updating the information and UI of their corresponding gameObject items.  We will work on them later but first, let's finish loading the data in the controller script which is the most important function/method while using EnhancedScroller.

/// <summary>
/// Load the data that will be used for updating the list of EnhancedScroller.
/// Initializing of the EnhancedScroller required components are done here
/// </summary>
public void LoadData()
{
    if (!_initialized)
    {
        _dataList = new SmallList<Data>();
        _dataListRefHeaderAndRow = new SmallList<BodyData>();
        scroller.Delegate = this;
        _initialized = true;
    }

    //clear the lists
    _dataList.Clear();
    _dataListRefHeaderAndRow.Clear();

    List<List<Data>> listOfCategoryItemList = new List<List<Data>>();

    //create and populate a list of a single category items with a single header
    List<Data> coinsList = new List<Data>();
    //populate the data with header and its item data
    HeaderData h1 = new HeaderData() {category = "coin"};
    coinsList.Add(h1);
    for (int i = 0; i < 5; i++)
    {
        coinsList.Add(new ItemData() {id = $"1213{i}", Text = $"Coin 10{i}"});
    }

    //add the populated list to the list of listItems
    listOfCategoryItemList.Add(coinsList);

    //create and populate a list of a single category items with a single header
    List<Data> gemsList = new List<Data>();
    //populate the data with header and its item data
    HeaderData h2 = new HeaderData() {category = "gems"};
    gemsList.Add(h2);
    for (int i = 0; i < 10; i++)
    {
        gemsList.Add(new ItemData() {id = $"121334{i}", Text = $"Coin 20{i}"});
    }

    //add the populated list to the list of listItems
    listOfCategoryItemList.Add(gemsList);


    //get the item limit per row to add the required number of row ref in the reference list of data
    int itemLimitPerRow = GetItemsLimitPerRow();
    //it is true when we need to add a body for items less than the item limit count of a row
    bool needToAddRowForLessItems = false;
    //updating the reference list for header and body row section

    foreach (var listOfItems in listOfCategoryItemList)
    {
        int totalItemAddInRow = 0;
        foreach (var item in listOfItems)
        {
            if (item is HeaderData)
            {
                //add the header reference to the reference list
                _dataListRefHeaderAndRow.Add(new HeaderData());

                //add the header data to the actual data holding list
                _dataList.Add(item);
            }
            else
            {
                //add the header data to the actual data holding list
                _dataList.Add(item);

                totalItemAddInRow++;
                //when limit of the item per row reach add new body row reference
                if (totalItemAddInRow == itemLimitPerRow)
                {
                    _dataListRefHeaderAndRow.Add(new BodyData());
                    totalItemAddInRow = 0;
                }
            }
        }

        //there may remain some items of the list then for them add another bodyrow reference
        if (totalItemAddInRow > 0)
        {
            _dataListRefHeaderAndRow.Add(new BodyData());
        }
    }

    //now reload the data in the enhance scroller
    scroller.ReloadData();
}
Initializing required components and loading the list with item data

In the above picture, you can see that at first we initialize the list and assign the script as the delegate of the EnhancedScroller. After the initializations, we have loaded the dummy data to the list holding actual data and another reference data holding list.

We have used a list of listItems representing the list of items of each category. The reason for doing so is that it makes the loading of reference data list easier and we can get the required number of items for the list that is to be shown by EnhancedScroller.

After loading all the data and creating the required list of actual data and reference list of the row items we need to reload the data of the Enhanced Scroller so it manages the item of the list as per our given list of data. So we have called scroller.ReloadData() in at the last when all the data are populated to the list as shown in the picture above. Now your main controller script is ready so let's create the remaining scripts.

[ scroller.delegate=this (delegate assignment) must be done before you call Reload() in EnhancedScroller. If you don't do this then your EnhancedScroller will not know which script to use for loading items to the list.]

3. EnhancedScrollerCellView:

It is a class provided by the EnhancedScroller and it is used by it for handling thegameobject items generated in the list. This is an essential class for EnhancedScroller. Without it, the enhancedScroller won't work. So be sure to make the script of the list item has EnhancedScrollerCellView as its extension class.

So let's continue our project and create the scripts for Items on the lists.

1. RowSectionView -> The class that will be used as an extension class for each row representing the script of the list. We have declared selectedDelegate delegate which provides ItemView as parameter its parameter in this class. We can declare it in any class you want but we have declared it here. This delegate will be used to provide ItemView's details to the CellViewListController when clicked and we can carry out any function we want.

/// <summary>
/// The delegate handles the selection or clicking of the shopItem
/// </summary>
/// <param name="cellView">The cell view that had the button click</param>
public delegate void SelectedDelegate(ItemView cellView);

/// <summary>
/// Base script represent the Section(a single horizontal) row in the enhance scroller list items.
/// </summary>
public class RowSectionView : EnhancedScrollerCellView
{
    
}
`RowSectionView` Base class for each row of list representer class

2. ItemView -> Script attached to the gameObject representing the items of the grid list. The data and UI is updated in this script using the SetData method this script given in the picture below.

/// <summary>
/// Base EnhanceScrollerCellView script of each list items.
/// </summary>
public class ItemView : EnhancedScrollerCellView
{
    /// <summary>
    /// The handler to call when this cell's button traps a click event
    /// </summary>
    public SelectedDelegate selected;

    /// <summary>
    /// Text reference of this item
    /// </summary>
    public Text ItemText;

    /// <summary>
    /// Reference to the underlying data driving this view
    /// </summary>
    private ItemData _data;

    private Button _button;

    private void Start()
    {
        if (_button == null)
        {
            _button = GetComponent<Button>();
            if (_button == null) _button = gameObject.AddComponent<Button>();

            _button.onClick.AddListener(OnSelected);
        }
    }

    /// <summary>
    /// This function just takes the data provide and displays it
    /// </summary>
    /// <param name="data"></param>
    public void SetData(int dataIndex, Data data, SelectedDelegate selected)
    {
        //update the provided delegate with the delegate of this script which will be triggered when button is clicked.
        this.selected = selected;

        //updating the text with the item number for its section.
        ItemText.text = $"Item {dataIndex + 1}";
    }

    /// <summary>
    /// This function is called by the cell's button click event
    /// This triggers the action we assigned when setting the data in`SetData`
    /// </summary>
    public void OnSelected()
    {
        // if a handler exists for this cell, then
        // call it.
        if (selected != null) selected(this);
    }
}
`ItemView` script for each item of a grid list

3. HeaderRowView -> Script attached to the header gameObject and is an extension class of RowSectionView class. It represents the header section among the rowSection script/class.  Its variables and methods are generally described in its summary in the picture.

/// <summary>
/// this script are used for the header type section of the list
/// </summary>
public class HeaderRowView : RowSectionView
{
    /// <summary>
    /// Data for holding the header section information
    /// </summary>
    private HeaderData _headerData;

    #region Drag and drop reference

    public Text Title;

    #endregion

    /// <summary>
    /// Set the data of the item and update the UI of this script
    /// </summary>
    /// <param name="dto"></param>
    public void SetData(ref SmallList<BodyData> rowList, int refListDataIndex, ref SmallList<Data> dataList,
        ref int startingIndex,
        SelectedDelegate selected)
    {
        if (dataList == null || dataList.Count == 0) return;

        #region updating the references of the indexes

        _headerData = rowList[refListDataIndex] as HeaderData;
        //update the index of the data of the list for later use 
        bool increaseIndexAtTheEnd = false;
        if (_headerData.dataRefSet)
        {
            startingIndex = _headerData.startingDataIndex;
        }
        else
        {
            //update the starting and ending index ref
            _headerData.startingDataIndex = startingIndex;

            increaseIndexAtTheEnd = true;
        }

        //replace the ref list row data
        rowList[refListDataIndex] = _headerData;

        #endregion

        #region updating the UI and actual data for udpating the UI

        //update the header data from the actual data list
        _headerData.category = (dataList[startingIndex] as HeaderData).category;
        //update the category of the dto as title text
        SetTitle(_headerData.category);

        #endregion

        if (increaseIndexAtTheEnd)
        {
            //increase the starting index for another row's starting index
            startingIndex++;
            _headerData.endingDataIndex = startingIndex;
            _headerData.dataRefSet = true;
        }
    }

    /// <summary>
    /// Set the tile using the title given in parameter
    /// </summary>
    /// <param name="title"></param>
    private void SetTitle(string title)
    {
        if (Title == null) return;
        Title.text = title;
    }

    /// <summary>
    /// Call when recycling
    /// </summary>
    public void Recycle()
    {
        _headerData = null;
    }
}
`HeaderRowView` script attachable to header GameObject

3. BodyRowView-> Script attached to the section of the row holding the multiple items. There will be many instances of this item for presenting all the available items of the category it comes under. Each of these items has a limit of certain items. So until the items of the same category are not shown, its instance will be shown by EnhancedScroller with the items that can be visible for that item.

/// <summary>
/// Body section where  the shop items(possible) are shown in horizontal row
/// </summary>
public class BodyRowView : RowSectionView
{
    /// <summary>
    /// Data holding the information of the body row section.
    /// It holds the information for the items this section will hold.
    /// </summary>
    private BodyData _bodyBody;


    public ItemView[] Items;

    /// <summary>
    /// This function just takes the Demo data and displays it
    /// </summary>
    /// <param name="data"></param>
    public void SetData(ref SmallList<BodyData> refRowList, int refListDataIndex, ref SmallList<Data> dataList,
        ref int startingIndex,
        SelectedDelegate selected)
    {
        if (dataList == null || dataList.Count == 0) return;
        _bodyBody = refRowList[refListDataIndex] as BodyData;
        //update the index of the data of the list for later use 
        if (_bodyBody.dataRefSet)
        {
            startingIndex = _bodyBody.startingDataIndex;
        }
        else
        {
            _bodyBody.startingDataIndex = startingIndex;
        }

        int totalItemGiven = dataList.Count;

        //update the actual data of the item to the now resized item of the array
        foreach (var item in Items)
        {
            bool updatingLimitReached = startingIndex >= totalItemGiven;

            var isShopItem = !updatingLimitReached && dataList[startingIndex] is ItemData;
            if (isShopItem)
            {
                item.SetData(startingIndex, dataList[startingIndex], selected);
                startingIndex++;
            }

            item.gameObject.SetActive(isShopItem && !updatingLimitReached);
        }

        _bodyBody.endingDataIndex = startingIndex;
        _bodyBody.dataRefSet = true;
        //replace the ref list row data
        refRowList[refListDataIndex] = _bodyBody;
    }

    /// <summary>
    /// Called when enhance scroller recycle this item
    /// </summary>
    public void Recycle()
    {
        //recycling when this item is disabled
        _bodyBody = null;
    }
}
`BodyRowView` row section holding items for each row of the grid layout
How data is updated in the header and body row section.

HeaderRowView and BodyRowView both scripts, when setting their data, will be provided with the list of the available item list, reference list of row section data, data starting index and the delegate to get the clicked item in return as given in the picture below.

public void SetData(ref SmallList<BodyData> rowList, int refListDataIndex, ref SmallList<Data> dataList,
    ref int startingIndex,
    SelectedDelegate selected)
Common method or `HeaderRowView` and `BodyRowView`

The explanation may not be fully understood but basically what each script of the row section of the list does is that it updates the index of the items, it will show, from list item _dataList to its BodyData data when data is set and then replace the data of the BodyData with the given reference list of the BodyData to update the data of the reference list. And then after updating data of the items each row can show it updates the refListDataIndex with the last item index corresponding to the _dataList and this very refListDataIndex will be used by another row to update its item's data.

Scene part

Create the required UI Items for the list

1. Create a panel for Enhanced Scroller and give it the size you want and attach EnhancedScroller to it as shown in the picture below.

attach_enhancedScroller_toPanel_LI.jpg
Panel with EnhancedScroller attached

2. Create a content placeholder and update it to the content reference of the ScrollRect as shown in the picture below. This content placeholder is only temporary. If you want you don't even need to create it as it will be removed by the EnhancedScroller when initiated. The purpose of this content is to create the items required to be shown in the content and create their prefab for updating the references of items in EnhancedScroller controlling script.

update_content_scrollRect_LI.jpg
Contentplaceholder of the EnhancedScroller panel

3. Create the header and body row items in the contentPlaceholder as shown in the picture below.

content_placeHolderAndheaderAndRow_LI.jpg
Creating `RowHeader` and `RowBody` items in the scene

4.  Add HeaderRowView script to the header item and update the references of the script. Drag and drop it to the asset and create its prefab so its references can be given to other scripts from the asset.

CellIdentifier of the script need to be populated with any name of your choice. If left empty it will give an error in playmode

add_header_script_LI.jpg
Adding `HeaderRowView` script to header and converting it to prefab

5. Add BodyRowView script to the Row:Body item and populate its references in the inspector. Don't forget to give cell Identifier a name and drag and drop the items that will be shown in the body row item to the `Items` list.

create_body_fill_items_create_prefab_LI.jpg
Adding `BodyRowView` script to body item and converting it to a prefab

6. Create an empty gameObject and add CellViewListController script to it and populate its references. The prefab references of header and body should be assigned from the asset folder, not from the gameObject of Hierarchy panel.

update_controllScritp_references_LI.jpg
Populating the references of the `CellViewListController` script from the editor

After doing all these things hit the play button in your unity editor and you will get the final form of a grid layout with a header for each section as shown in the first picture of this article.

There are many types of list layout that can be done using EnhancedScroller and hope you try them out and check how easy it is to use EnhancedScroller of handling the list of different types.

Thank you for reading upto last. Please leave comments if you've any queries.