All of the examples extend ListViewController, ListViewItem, and ListViewItemData to suit their needs, and override a number of ListViewController classes where they need to.
The first example doesn’t actually make use of the framework code. It illustrates the simplest possible example of a list that follows these design guidelines. Scene objects are re-used, and there is no event to update the list -- it is updated every frame based on the current value of dataOffset. The beauty of this set-up is that there is no difference between scrolling just one element at a time, or jumping to a different place within the list. We also don’t care if only a few rows are visible, since we just fall back to an empty string if we have no data.
This example uses the vanilla ListViewController implementation. The only reason we extend the class is to add an OnGUI function to display the instructions and scroll buttons. Unlike our simple example, the list is set up to scroll smoothly, meaning that rather than modifying the data offset directly, we modify a positional offset, from which we derive the data offset. This happens in ListViewController.cs:66. The following if statement is interesting. Because we are casting the result of scrollOffset / itemSize.x, we actually get a value of “0” for the entire range of (-1, 1). We want to “tick” the data offset when the scroll offset changes from positive to negative, so for all negative values of scrollOffset, we subtract one from data offset.
The default positioning function will arrange items horizontally. By adding the total scroll offset to the product of the row index and the width of an item, we get items that are evenly spaced, and move together in unison when the scroll offset changes. Essentially we are adding scrollOffset % itemWidth.x to each item’s position, since dataOffset = scrollOffset / itemWidth.x.
There are three prefabs to act as item templates, which are just the default cube, capsule, and sphere from the Create menu, with a ListViewItem component. All three of them go into the templates array via the inspector. The data array uses ListViewInspectorData so that we can set up the data in the inspector. All we need to do is set the desired size and fill in a template value for each row that matches the name of one of the prefabs in the templates array.
2. Custom Scrolling
The second example is an almost-identical copy of the first, but adds a ListViewMouseScroller, which uses a raycast to detect when the user clicks and drags on an item in the list. The abstract ListViewScroller class defines how scrolling interaction should work, and ListViewMouseScroller simply overrides HandleInput to call the scrolling functions with mouse data. The process is simple: When we detect that an object is clicked, we store the current scrollOffset and the position where the mouse ray hit the object. For every subsequent frame while the mouse button is held, we add the horizontal distance between the first click and the current mouse position to the stored scroll offset, and scroll to that offset. When the mouse button is released, we stop changing the scroll offset.
3. Custom Item
This example builds upon the first two by introducing a text label to the item template. To do this, we need to extend ListViewItem to add a reference to the label (TextMesh) and we need to extend ListViewItemData to add a string variable to store the value. Again, we’ll just be using the inspector to set up the array of data. In this case, we could have put whatever text we wanted into the data rows, but out of convenience we just use a loop in Setup to store the index of the row as a string in CubeItemData.text. When the list needs to display an item, it calls CubeItem.Setup which sets the value of the textmesh to the value of CubeItemData.text.
4. JSON Data
Most of the time, you won’t want to serialize your data in a scene or prefab. Even if you are storing the data locally, it is helpful to keep it in a human-readable file that doesn’t need to be built into the game. This example uses a free JSON library from the Asset Store to read the data from a text file into the data array. It overrides ListViewItemData to add a FromJSON method which will create a row from a JSONObject.
5. Nested Data
Up until this point, the topology of our data has always been a flat structure. That is to say, we just have a bunch of items that are always supposed to show up in a given order. Something like a filesystem, scene hierarchy, or maybe a family tree would require some data to be grouped “inside” or “underneath” other data, which is usually displayed in such a way that you can expand or collapse groups to reveal their children. This is possible with our framework, but it can result in some strange behavior if you use features like ScrollTo, which assumes that each list item will always have the same index within the list.
NestedJSONList overrides UpdateItems to call a recursive function which does basically the same thing. The only difference is that instead of comparing dataOffset to an array index, it uses the total count of recursive iterations, which takes into account previously encountered child elements of earlier groups. If an item has children and its “expanded” parameter is set to true, we call UpdateRecursively on its list of children, which will cause them to show up as the next items in the list.
The last thing we need is a RecycleChildren function to clean up child objects of a recently-collapsed group.
6. Web Data
So far, all of our examples have stored their data locally. However, we often need to display information from an online resource. Remote servers can store virtually infinite amounts of data, and can be updated independently of local installs. There are many reasons why one might want to list web-driven data, and a few considerations which must be made.
The first, and hopefully most obvious consideration is that web data cannot be accessed “instantly.” In our previous examples, it was totally fine for our code to block on fetching items, both when deserializing the entire list, as well as accessing individual rows from the data array. All network requests come with some latency, especially HTTP requests over the Internet. As such, we need to account for a delay between when the list “wants,” the data and when the data is actually returned. The solution, of course, is to cache a segment of this data into our array so that we can work with it normally, and pay attention for whenever we get near the beginning or end of our cache, so that we can fetch the next segment.
We also need a web service that we can break up into chunks. It is not strictly necessary to know the total size of the data-set, since that is only important once we finally scroll to the end of the list. Actually, if you just ignore the possibility that the list can end, the default behavior is like an “infinite scroll” design. Whenever you get near the end of the list, you fetch more data, and keep scrolling through that. In any case, you need a way to specify how many results you want, and the starting offset, or how many results to “skip over,” for every segment after the first one. Some services ask for the last key of the previous segment, rather than a numeric index, which can make things pretty tricky. Surprisingly, not all web services make it convenient to request a specific segment of data.
This is the first example to display items vertically, so we override the Positioning function in WebList and Scroll function in WebListInputHandler to use Y-components instead of X-components. ComputeConditions also changes slightly for a vertical list, and of course contains the logic for deciding when to fetch the next batch of data. Pay special attention to the use of the webLock and loading variables to keep track of whether a fetch is currently in progress. We only increment batchOffset when we get a new batch of data, and don’t initiate any more fetches until he previous fetch is done. The idea is to keep three batches of data cached locally, like a “moving window” into the larger data set. As soon as we enter the third batch, we start fetching the next one. Once we get the fourth batch, we replace the data array with batches two through four, and increment batchOffset. If we are past the first “window” of three batches, entering the first cached batch triggers the opposite procedure. We fetch the previous batch, add it to the beginning of the array, discarding the third batch, and decrement batchOffset. If we are scrolling too quickly, or jumping to an offset outside of our local cache, we enter a “loading” state, and fetch a whole new cache of three batches, centered around the current dataOffset.
7. Advanced List
We’re back to local data, but in this case we’re going crazy with templates. This example shows of how to use two templates of different sizes, nesting, and just for fun stick a prefab onto some of the list items. We also introduce a depth mask to control the outline of the list view on-screen.
This example also shows how to limit scrolling to the extents of the list, preventing the user from scrolling the whole list off-screen and not being able to get it back. In the case of the top of the list, we simply check whether scrollOffset has gone positive, and set it back to 0 when you stop scrolling. For the end of the list, we detect the moment that the magnitude of scrollOffset goes above the total height of the elements in the list, minus the last one. To get this total height, we sum up all of the item heights as we recursively iterate over the list, since items have different sizes and may or may not be collapsed. As soon as the offset exceeds this threshold, we store the threshold value, and, again, when the user stops scrolling, we set scrollOffset to the stored value. This scrolls the list back to a state where only the last item is visible, so that the user can click on it and scroll back up.
The purpose of this example is to show off a very large local data set. It turns out to be the same solution as the web example, since our main concern is the latency overhead that comes with accessing the data source. There are some plugin files and code to establish the database connection (based on a Unity Answers page), but otherwise the code is pretty much identical to the web example. Note that you will have to switch Api Compatibility Level from .NET 2.0 Subset to .NET 2.0 in Player Settings, or player builds will fail.
Because we’re using a huge dataset, it is especially helpful in this example to add momentum to the scrolling behavior. All we have to do is keep track of last frame’s scrollOffset, and we can compute the difference between that and the current value, giving us a current velocity. When we aren’t scrolling, we add the velocity to scrollOffset, and slowly decrease the velocity by a damping value.
Since the project was inspired by a playing cards metaphor, we might as well have have a cards example! The Card class demonstrates a particularly complicated Setup LPfunction, which is responsible for creating the face of the card based on its value. This was a bit more work than it might have been to just grab 52 different card textures and call it a day, but we’re saving a lot of GPU memory and potentially a few draw calls, depending on how many cards are visible at a time. If you’re interested in extra credit, consider adding code to pool the quad objects, rather than instantiating/destroying them every time we call Setup. =)
The true novelty to the Cards example is how they animate as you scroll them. The cards appear to come out of one deck and stack up on the other. This is accomplished by using Vector3.Lerp in the Positioning function, setting the position/rotation of the card when it becomes visible, and using a coroutine in the Recycle method to play a brief animation before deactivating the object. We also use a Quaternion.Lerp to animate the rotation of the cards between their initial state and the target rotation in the list.
This particular technique for animating transitions works very well if you have a programmer brain, but designers and artists might prefer to specify animation curves and blend actual animation clips together. This requires some extra code in the ListItem behavior to keep track of animation states, but the general idea would be to pass off control of the item’s position between the positioning function and the item’s animator. Another possibility would be to use local offsets within the animation clips, letting the ListViewController position a parent transform as normal.
10. Card Game
Finally, a game! Or at least the beginnings of one… This final example uses the same data and item behaviors as the previous example. It doesn’t actually set up a scrollable list, but rather a hand of cards and a “River” next to the deck, like in Texas Hold’em poker. The CardGameList class is set-up to left-align the card list, and rather than smoothly scrolling side-to-side, we change the range value to add another card to the right. Once we reach the max number of cards (set to 5), we set the range back down to zero and scroll 5 cards over. When we reach the end of the deck, we shuffle again, and set scrollOffset back to zero. Actually, we use a scroll offset of half a card-width in order to center the cards within the range.
The CardGameHand class is also a ListViewController, but it’s set up to be a slave of the CardGameList object, sharing its pool of card objects, and able to call methods on CardGameList for drawing and discarding cards. As in the previous example, all of the animations are done simply by interpolating position and rotation between the current value and a target transform. Just for fun, we have a single rule: no more than 5 cards in your hand. If you try to draw a sixth card, a red sphere becomes visible for a fraction of a second to indicate that this move is not allowed. Isn’t that fun?! But seriously, given this example as a template, it should be pretty easy to whip up a poker game. I look forward to seeing what you can do with it!