Skip to content

Instantly share code, notes, and snippets.

@dawsontoth
Created September 8, 2011 07:15
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save dawsontoth/209ca96d7fa64f3f2101 to your computer and use it in GitHub Desktop.
Save dawsontoth/209ca96d7fa64f3f2101 to your computer and use it in GitHub Desktop.
Lazy Loaded Table
/**
* In this code sample, we will make a lazy loaded table. The "LazyLoadedTable.js" file is well documented, but I will
* mention briefly what it does:
*
* 1) Rows are flyweights, so we only need 10-20 of them no matter how many total rows there are.
* 2) When a row is "visible" we will populate one of the row flyweights with the row-specific data.
*
* This results in a very lightweight table where we hardly ever need to create new data. All we're dealing with is
* changing strings and images, instead of having to create thousands upon thousands of rows of data.
*
* Read up on flyweights here: http://en.wikipedia.org/wiki/Flyweight_pattern
*/
var win = Ti.UI.createWindow({ backgroundColor: '#fff' });
Ti.include('LazyLoadedTable.js');
// In our table, we can check individual rows.
// So we'll keep track of which ones are checked in this variable:
var checkedRows = {};
// Now we create the lazy loaded table. Take a look at the LazyLoadedTable.js to find out what each parameter does.
var table = new LazyLoadedTable({
rowHeight: 60,
rowCount: 1000,
rowBuffer: 3,
properties: {
top: 0, right: 50, bottom: 0, left: 0,
showHorizontalScrollIndicator: false,
showVerticalScrollIndicator: true
},
createReusableRow: function() {
var preventClick = false;
var row = Ti.UI.createView({
height: 60, left: 0, right: 0, isChecked: false,
backgroundColor: '#fff',
// Note this special "rowIndex" property. This will automatically get populated with the index of the
// row we are looking at. So in our event listeners, we can reference this to find out which row we
// are interacting with.
rowIndex: 0
});
var check = Ti.UI.createImageView({
image: 'images/cancel.png',
left: -5, top: 0,
width: 80, height: 40,
backgroundColor: '#fff'
});
row.add(check);
// Now note that we are storing a reference to the "row.syncChecked" function directly on the row itself.
// We do this because "populateRow" will only receive an index and a row, and we want to be able to call
// this function later to make sure the image is correct!
row.syncChecked = function() {
check.image = 'images/' + (row.isChecked ? 'ok' : 'cancel') + '.png';
};
check.addEventListener('click', function() {
row.isChecked = !row.isChecked;
row.syncChecked();
checkedRows[row.rowIndex] = row.isChecked;
preventClick = true;
});
// Here's another place where we store a reference to the "title" label on the row itself.
row.add(row.title = Ti.UI.createLabel({
font: { fontSize: 18, fontWeight: 'bold' },
top: 5, left: 50, right: 30,
height: 20,
backgroundColor: '#fff'
}));
// And here's another: we do this so we can update the labels' text!
row.add(row.desc = Ti.UI.createLabel({
font: { fontSize: 12 },
top: 25, left: 50, right: 30,
height: 30,
backgroundColor: '#fff'
}));
row.add(Ti.UI.createImageView({
image: 'images/arr6.png',
right: 10,
width: 9, height: 13,
backgroundColor: '#fff'
}));
row.add(Ti.UI.createView({
backgroundColor: '#ddd',
bottom: 0, left: 0, right: 0,
height: 1
}));
row.addEventListener('click', function() {
if (preventClick) {
preventClick = false;
}
else {
// Notice here that we leverage the "row.rowIndex" property! This will automatically get populated with
// the current row's index.
alert('You clicked row ' + row.rowIndex);
}
});
return row;
},
populateRow: function(row, index) {
// Remember above when we stored a reference to the title and desc labels on the row itself?
// We can now use those to update the text of the labels with information specific to the rows!
row.title.text = 'Row ' + index;
row.desc.text = 'This is some additional information about the row. Fascinating, isn\'t it?';
row.isChecked = checkedRows[index];
row.syncChecked();
// UNCOMMENT NEXT TWO LINES TO SIMULATE iPad 1 PERFORMANCE:
//var time = new Date().getTime();
//while (new Date().getTime() < time + 100) {}
}
});
win.add(table.retrieveView());
// Let's also demonstrate how to quickly jump between rows in our lazy loaded table. You could quickly build up
// a dynamic index to make it easy for your users to jump around in your data.
// We'll manually create two: one to jump to the top.
var top = Ti.UI.createButton({
title: 'Top',
top: 0, right: 0,
width: 50, height: 30
});
top.addEventListener('click', function() {
table.scrollTo(0);
});
win.add(top);
// And another to jump to the 900th row.
var bottom = Ti.UI.createButton({
title: '900',
bottom: 0, right: 0,
width: 50, height: 30
});
bottom.addEventListener('click', function() {
table.scrollTo(900);
});
win.add(bottom);
// To get our table started, we need to call "reloadVisible" when the window first opens. This kick starts the table
// and shows the visible rows to the user. Note that you could use this function to force the table to reload its data
// if something changed in your data source -- like the sorting of the table, or the text that you want to output.
win.addEventListener('open', function() {
table.reloadVisible();
});
// We'll also explicitly listen for the close event so that we can ask the table to clean up after itself.
win.addEventListener('close', function() {
table.dispose();
});
// And that's it! I hope this helps you get an idea for how you can show a lot of data to your users without having
// to create a ton of objects.
win.open();
/**
* Creates a lazy loading table that reuses rows. This is designed to be light on memory and very quick to load.
* @param args A dictionary containing the following keys:
* - int rowHeight: The height of one row. Use the "changeRowHeight" method to change this value after creation.
* - int rowCount: The total number of rows your table will have. Use the "changeRowCount" method to change this value
* after creation.
* - int rowBuffer: The number of rows to buffer outside of the purely visible region. This only applies to downward rows.
* - Dictionary properties: The view properties for the table, such as top, left, backgroundColor, etc.
* - function createReusableRow(): A function that creates a reusable row. Note that this row is a flyweight, and
* row-specific data will be loaded in the "populateRow" function you provide below.
* - function populateRow(row, index): A function that receives a row and an index, and is expected to update the row
* with its specified index-based-data. Keep this function as light as possible, it will be called frequently!
*/
function LazyLoadedTable(args) {
/***********************
* Instance Variables
***********************/
var rowHeight = args.rowHeight, rowCount = args.rowCount, rowBuffer = args.rowBuffer || 10;
var scroll = Ti.UI.createScrollView(args.properties || {});
var firstVisibleRowIndex, lastVisibleRowIndex;
var recycledRows = [], visibleRowMap = {};
var loadFromTop = true, lastOffset = 0;
var stop = true;
/***********************
* Utility Functions
***********************/
/**
* When we start scrolling, the table should start updating itself.
*/
function startUpdating() {
stop = false;
redrawTable();
}
/**
* Detect which way we are scrolling so that the rows can load in the most beneficial direction.
*/
function detectScrollDirection() {
var currentOffset = (scroll.contentOffset || { y: 0 }).y;
loadFromTop = currentOffset < lastOffset;
lastOffset = currentOffset;
}
/**
* After scrolling ends, stop updating the table. Nothing is changing!
*/
function stopUpdating() {
stop = true;
}
/**
* Redraws our table.
*/
function redrawTable() {
determineRowVisibility();
recycleInvisibleRows();
createVisibleRows();
if (!stop)
setTimeout(redrawTable, 100);
}
/**
* When the user scrolls, delay a redraw of the table. This will let us defer the loading until after they have
* stopped their scroll.
*/
function handleScroll() {
redrawTable();
}
/**
* Updates the scrollable height of the table.
*/
function updateScrollHeight() {
if (rowHeight && rowCount) {
scroll.contentHeight = rowHeight * rowCount;
}
}
/**
* Determines the range of rows that are visible (includes the buffer).
*/
function determineRowVisibility() {
firstVisibleRowIndex = parseInt((scroll.contentOffset || { y: 0 }).y / rowHeight, 10);
if (firstVisibleRowIndex < 0) {
firstVisibleRowIndex = 0;
}
var visibleRowsCount = parseInt(scroll.size.height / rowHeight, 10) + 1;
lastVisibleRowIndex = firstVisibleRowIndex + visibleRowsCount;
if (lastVisibleRowIndex >= rowCount) {
lastVisibleRowIndex = rowCount - 1;
}
}
/**
* Recycles any rows that are not currently visible.
*/
function recycleInvisibleRows() {
for (var key in visibleRowMap) {
var rowIndex = visibleRowMap[key].rowIndex;
if (rowIndex < firstVisibleRowIndex || rowIndex > lastVisibleRowIndex) {
recycledRows.push(visibleRowMap[rowIndex]);
delete visibleRowMap[rowIndex];
}
}
}
/**
* Checks if a row needs to be updated, and updates it if necessary.
* @param rowIndex
*/
function checkRow(rowIndex) {
if (visibleRowMap[rowIndex]) {
return;
}
var row = recycledRows.pop();
if (!row) {
row = args.createReusableRow();
scroll.add(row);
}
row.rowIndex = rowIndex;
row.top = rowIndex * rowHeight;
args.populateRow(row, rowIndex);
visibleRowMap[rowIndex] = row;
}
/**
* Makes sure that a row exists for all of the currently visible rows.
*/
function createVisibleRows() {
var increment = loadFromTop ? 1 : -1;
var start = loadFromTop ? firstVisibleRowIndex : lastVisibleRowIndex;
var end = (loadFromTop ? lastVisibleRowIndex : firstVisibleRowIndex) + increment;
for (var rowIndex = start; rowIndex != end; rowIndex += increment) {
checkRow(rowIndex);
}
}
/***********************
* Public API
***********************/
/**
* Retrieves the view for this table. You can then add this view to your view hierarchy.
*/
this.retrieveView = function() {
return scroll;
};
/**
* Changes the standard row height that you specified in the creation dictionary.
* @param newHeight
*/
this.changeRowHeight = function(newHeight) {
if (rowHeight == newHeight) {
return;
}
rowHeight = newHeight;
updateScrollHeight();
this.reloadVisible();
};
/**
* Changes the standard row count that you specified in the creation dictionary.
* @param newCount
*/
this.changeRowCount = function(newCount) {
if (rowCount == newCount) {
return;
}
rowCount = newCount;
updateScrollHeight();
this.reloadVisible();
};
/**
* Reloads all of the visible rows. If your data source were to change, you could call this to force the table to
* fetch the data for the rows again.
*/
this.reloadVisible = function() {
lastVisibleRowIndex = -1;
recycleInvisibleRows();
redrawTable();
};
/**
* Scrolls to a particular row index in the table.
* @param rowIndex
*/
this.scrollTo = function(rowIndex) {
scroll.scrollTo(0, rowIndex * rowHeight);
redrawTable();
};
/**
* Cleans up the resources that the table was using.
*/
this.dispose = function() {
lastVisibleRowIndex = -1;
recycleInvisibleRows();
while (recycledRows.length) {
scroll.remove(recycledRows.pop());
}
scroll.removeEventListener('dragStart', startUpdating);
scroll.removeEventListener('scroll', detectScrollDirection);
scroll.removeEventListener('scrollEnd', stopUpdating);
recycledRows = null;
visibleRowMap = null;
scroll = null;
};
/***********************
* Initialization
***********************/
updateScrollHeight();
scroll.addEventListener('dragStart', startUpdating);
scroll.addEventListener('scroll', detectScrollDirection);
scroll.addEventListener('scrollEnd', stopUpdating);
return this;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment