-
-
Save dawsontoth/209ca96d7fa64f3f2101 to your computer and use it in GitHub Desktop.
Lazy Loaded Table
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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