Endless scroller
This example demonstrates how we can create an endless scrolling list that removes elements as they scroll out of view.
When making large lists, we use Each
and data-bind its Items
property to an Observable list in JavaScript. When making infinite lists, we have to avoid extensive memory use by employing two less known properties available on Each
: Limit
and Offset
. By setting the Limit
we limit the number of items being output by the Each
at any given moment, and by changing the Offset
we can move through our list of items forward and back.
<Each Items="{list}" Offset="{offset}" Limit="{limit}">
We want to trigger the offset change at the top and bottom of the ScrollView
, and we achieve this by using a pair of Scrolled
triggers which activate when the user scrolls to a certain distance from either of the edges. We bind the Handler
properties to JavaScript functions that manipulate an offset variable.
<ScrollView LayoutMode="PreserveVisual">
<Scrolled To="Start" Within="104" Handler="{decreaseOffset}" />
<Scrolled ux:Name="atEnd" To="End" Within="104" Handler="{increaseOffset}" />
</ScrollView>
The Within
property sets how close to the start or end of the ScrollView
the user has to be for the trigger to be activated. Depending on the complexity of each item and the amount of items we add, we might want to adjust the Within
property to trigger earlier (or later) to give the app more time to create all the elements.
It is important that we set the limit
variable high enough so that the items take up a big enough space in the ScrollView
to avoid unnecessary Scrolled
triggering as we change the offset
. We must also change the offset
by a number that is just right: low enough so that the list can be pre-populated quickly, and high enough to avoid loading new items too frequently.
var limit = 20;
var offset = Observable(0);
function increaseOffset() {
changeOffset(5);
}
function decreaseOffset() {
changeOffset(-5);
}
function changeOffset(diff) {
var nextOffset = offset.value + diff;
if (nextOffset >= 0) {
offset.value = nextOffset;
if ((list.length - nextOffset) <= limit) {
loadItems().then(function() {
atEnd.check();
});
}
}
}
If our users scroll really fast on a slow internet connection, they can hit the bottom of our ScrollView
while we’re still busy appending items to the list. To work around that, we wrap the loading of new items in a Promise-function and as seen in the changeOffset
function above, we check if we’re at the end of our ScrollView
when it’s done. In this example, we use a simple list of image IDs from unsplash.com.
var images = [
"eWFdaPRFjwE","_FjegPI89aU","_RBcxo9AU-U","PQvRco_SnpI","6hxvm0NzYP8",
"b-yEdfrvQ50","pHANr-CpbYM","45FJgZMXCK8","9wociMvaquU","tI_Odb7ZU6M",
"o0RZkkL072U","N-sxA8vEGDk","324ovGl1BwM","RSOxw9X-suY","qv5yb436qRI",
"iWFRUyqpCbQ","ZJ4yhJFIzaY","ze0tqwj86S4","gQR4STZ24kM","xMYnPo53ukE"
];
function loadItems() {
var p = new Promise(function(resolve) {
var items = images.map(function(i) {
return {image: "https://source.unsplash.com/" + i + "/416x208"};
});
list.addAll(items);
resolve();
});
return p;
}
When it comes to displaying items, we data-bind the Limit
and Offset
properties on the Each
to the JavaScript variables we created. This allows us to limit the number of items output by the Each
and move through the list as we scroll up and down.
<Each Items="{list}" Offset="{offset}" Limit="{limit}">
<ListItem />
</Each>
The ListItem
component uses Deferred
tag to prevent lag-spikes due to addition of new items by allowing Fuse to spread the work over multiple frames. Coupled with an AddingAnimation
, this results in a subtle fade-in transition as the images arrive. Note that we set a MemoryPolicy="UnloadUnused"
on the ImageFill
tag to make sure memory is freed from the images that are off-screen.
<Panel ux:Class="ListItem" Height="104">
<Deferred>
<Rectangle ux:Name="imageHolder" CornerRadius="2" Opacity="1">
<AddingAnimation>
<Change imageHolder.Opacity="0" Delay="0.16" Duration="0.32" />
</AddingAnimation>
<ImageFill Url="{image}" StretchMode="UniformToFill" WrapMode="ClampToEdge" MemoryPolicy="UnloadUnused" />
</Rectangle>
</Deferred>
<Rectangle CornerRadius="2" Color="#0003" />
</Panel>
That’s it! Feel free to download the source code and play around.