Created
July 21, 2013 13:28
-
-
Save dcporter/6048580 to your computer and use it in GitHub Desktop.
A quick-and-dirty BestFitGridView.
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
// A (quick and dirty) view which renders its content as a best-fit grid – that is, it shows all items, | |
// laid out as close to evenly as possible. | |
// Known issues: | |
// - doesn't update when content changes (wasn't part of my needs); easy observer fix | |
// - isn't a subclass of SC.CollectionView | |
// - someone with the maths could optimize the calculations | |
// - child views are never destroyed, only created and cached | |
BestFitGridView = SC.View.extend({ | |
content: null, | |
length: null, | |
lengthBinding: SC.Binding.oneWay('*content.length'), | |
// When we get visible, or resize while visible, we lay everything out. | |
layoutDidChange: function() { | |
if (!this.getPath('parentView.isVisible')) return; | |
var frame = this.get('frame'), | |
n = this.get('length'); | |
if (!frame || !n) return; | |
var ratio = frame.height / frame.width; | |
// For each number from 1 up to the next square above the slot count's square root, test the | |
// ratios. The one that's closest to the ratio of the screen size wins. | |
// For example, eight items will give a next square root of 3, and trigger ratio comparisons | |
// of 1 x 8, 2 x 4, 3 x 3, 4 x 2 and 8 x 1. If the view is currently 400 x 800, then 2 x 4 will | |
// match most closely. | |
var nextSqrt = Math.ceil(Math.sqrt(n)), | |
rows, cols, winningRatio, | |
thisRows, thisCols, thisRatio; | |
for (var i = 1; i <= nextSqrt; i++) { | |
// x by y. | |
thisRows = i; | |
thisCols = Math.ceil(n / i); | |
thisRatio = thisRows / thisCols; | |
if (!winningRatio || Math.abs(thisRatio - ratio) < Math.abs(winningRatio - ratio)) { | |
rows = thisRows; | |
cols = thisCols; | |
winningRatio = thisRatio; | |
} | |
// y by x. | |
thisCols = i; | |
thisRows = Math.ceil(n / i); | |
thisRatio = thisRows / thisCols; | |
if (!winningRatio || Math.abs(thisRatio - ratio) < Math.abs(winningRatio - ratio)) { | |
rows = thisRows; | |
cols = thisCols; | |
winningRatio = thisRatio; | |
} | |
} | |
// Finally, assemble the metrics object, and if it's different than the current one, | |
// set it. | |
var metrics, newMetrics; | |
metrics = this.get('metrics'); | |
newMetrics = { | |
rows: rows, | |
cols: cols | |
}; | |
if (metrics.rows !== newMetrics.rows || metrics.cols !== newMetrics.cols) { | |
this.set('metrics', newMetrics); | |
} | |
}.observes('.parentView.isVisible', 'frame'), | |
// The results. (Mostly for observing.) | |
metrics: {}, | |
// --------------------- | |
// Rendering | |
// | |
// Override the exampleView with something interesting. | |
exampleView: SC.View.extend({ | |
backgroundColor: 'lightblue' | |
}), | |
viewCache: [], | |
contentOrMetricsDidChange: function() { | |
// Get data. | |
var content = this.get('content') || [], | |
metrics = this.get('metrics'); | |
if (!metrics) return; | |
var rows = metrics.rows, | |
cols = metrics.cols; | |
// Calculate global metrics (in percent). | |
var height = 1 / rows, | |
width = 1 / cols; | |
// Can't have any 100%s, as SC thinks that means 1 pixel. Tweak down. | |
if (height === 1) height = 0.99999; | |
if (width === 1) width = 0.99999; | |
// Get scope variables ready. | |
var index, | |
view = this, | |
viewCache = this.viewCache; | |
// Loop through. | |
content.forEach(function(item, i) { | |
index = i; | |
// Maths. | |
var r = Math.floor(i / cols), | |
c = i % cols; | |
// Get child view. | |
var cv = viewCache[i]; | |
if (!cv) { | |
cv = view.createChildView(view.exampleView); | |
viewCache[i] = cv; | |
} | |
// Apply metrics and data. | |
cv.set('slot', item); | |
cv.adjust({ top: r * height, left: c * width, height: height, width: width }); | |
cv.set('isVisible', YES); | |
// Append if necessary. | |
view.appendChild(cv); | |
}); | |
// Hide extra child views. | |
for (index++; index < this.viewCache.length; index++) { | |
if (this.viewCache[index]) this.viewCache[index].set('isVisible', NO); | |
} | |
}.observes('content', 'metrics') | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment