Skip to content

Instantly share code, notes, and snippets.

@dcporter
Created July 21, 2013 13:28
Show Gist options
  • Save dcporter/6048580 to your computer and use it in GitHub Desktop.
Save dcporter/6048580 to your computer and use it in GitHub Desktop.
A quick-and-dirty BestFitGridView.
// 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