Skip to content

Commit

Permalink
Added default simple layout algorithm for Masonry (createCellPositioner)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed Mar 26, 2017
1 parent 6aff655 commit f3f6867
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 88 deletions.
92 changes: 61 additions & 31 deletions docs/Masonry.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Phase one is repeated if the user scrolls beyond the current layout's bounds. If
|:---|:---|:---:|:---|
| cellCount | number || Total number of items |
| cellMeasurerCache | mixed || Caches item measurements. Default sizes help `Masonry` decide how many images to batch-measure. Learn more [here](CellMeasurer.md#cellmeasurercache). |
| cellPositioner | function || Positions a cell given an index: `(index: number) => ({ left: number, top: number })` |
| cellPositioner | function || Positions a cell given an index: `(index: number) => ({ left: number, top: number })`. [Learn more](#createmasonrycellpositioner) |
| cellRenderer | function || Responsible for rendering a cell given an index. [Learn more](#cellrenderer) |
| className | string | | Optional custom CSS class name to attach to root `Masonry` element. |
| height | number || Height of the component; this value determines the number of visible items. |
Expand Down Expand Up @@ -70,54 +70,84 @@ function cellRenderer ({
}
```

### createMasonryCellPositioner

`Masonry` provides a built-in positioner for a simple layout. This positioner requires a few configuration settings:

| Property | Type | Required? | Description |
|:---|:---|:---:|:---|
| cellMeasurerCache | `CellMeasurerCache` || Contains cell measurements (eg item height). |
| columnCount | number || Number of columns to use in layout. |
| columnWidth | number || Column width. |
| spacer | number | | Empty space between columns; defaults to 0. |

You can use this layout as shown below:

```js
const cellPositioner = createMasonryCellPositioner({
cellMeasurerCache: cache,
columnCount: 3,
columnWidth: 200,
spacer: 10
})

let masonryRef

function renderMasonry (props) {
return (
<Masonry
cellMeasurerCache={cache}
cellPositioner={cellPositioner}
ref={ref => masonryRef = ref}
{...props}
/>
)
}
```

If any of the configuration settings change due to external changes (eg window resize event) you can update them using the `reset` method as shown below:

```js
cellPositioner.reset({
columnCount: 4,
columnWidth: 250,
spacer: 15
})

masonryRef.recomputeCellPositions()
```

### Basic `Masonry` Example

Below is a very basic `Masonry` example with a naive layout algorithm.

```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { CellMeasurer, CellMeasurerCache, Masonry } from 'react-virtualized';
import {
CellMeasurer,
CellMeasurerCache,
createMasonryCellPositioner,
Masonry
} from 'react-virtualized';

// Array of images with captions
const list = [];

// Our masonry layout will contain 3 columns
const columnCount = 3

// Track the heigh of each of our 3 columns
const columnHeights = {}

// Default sizes help Masonry decide how many images to batch-measure
const cache = new CellMeasurerCache({
defaultHeight: 250,
defaultWidth: 200,
fixedWidth: true
})

function cellPositioner (index) {
// Super naive Masonry layout
let columnIndex = 0
if (index < columnCount) {
columnIndex = index
} else {
for (let index in columnHeights) {
if (columnHeights[index] < columnHeights[columnIndex]) {
columnIndex = index
}
}
}

const left = columnIndex * (columnWidth + gutterSize)
const top = columnHeights[columnIndex] || 0

columnHeights[columnIndex] = top + gutterSize + cache.getHeight(index)

return {
left,
top
}
}
// Our masonry layout will use 3 columns with a 10px gutter between
const cellPositioner = createMasonryCellPositioner({
cellMeasurerCache: cache,
columnCount: 3,
columnWidth: 200,
spacer: 10
})

function cellRenderer ({ index, key, parent, style }) {
const datum = list[index]
Expand Down
1 change: 1 addition & 0 deletions source/Masonry/Masonry.example.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
border-radius: .5rem;
padding: 0.5rem;
background-color: #f7f7f7;
word-break: break-all;
}
57 changes: 27 additions & 30 deletions source/Masonry/Masonry.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ContentBox, ContentBoxHeader, ContentBoxParagraph } from '../demo/Conte
import { LabeledInput, InputRow } from '../demo/LabeledInput'
import { CellMeasurer, CellMeasurerCache } from '../CellMeasurer'
import AutoSizer from '../AutoSizer'
import createCellPositioner from './createCellPositioner'
import Masonry from './Masonry'
import styles from './Masonry.example.css'

Expand Down Expand Up @@ -32,7 +33,6 @@ export default class GridExample extends PureComponent {
gutterSize: 10
}

this._cellPositioner = this._cellPositioner.bind(this)
this._cellRenderer = this._cellRenderer.bind(this)
this._onResize = this._onResize.bind(this)
this._renderMasonry = this._renderMasonry.bind(this)
Expand Down Expand Up @@ -81,6 +81,7 @@ export default class GridExample extends PureComponent {
columnWidth: parseInt(event.target.value, 10) || 200
}, () => {
this._calculateColumnCount()
this._initOrResetPositioner()
this._masonry.clearCellPositions()
})
}}
Expand All @@ -95,6 +96,7 @@ export default class GridExample extends PureComponent {
gutterSize: parseInt(event.target.value, 10) || 10
}, () => {
this._calculateColumnCount()
this._initOrResetPositioner()
this._masonry.recomputeCellPositions()
})
}}
Expand Down Expand Up @@ -122,35 +124,6 @@ export default class GridExample extends PureComponent {
this._columnCount = Math.floor(this._width / (columnWidth + gutterSize))
}

_cellPositioner (index) {
const {
columnWidth,
gutterSize
} = this.state

// Super naive Masonry layout
let columnIndex = 0
if (index < this._columnCount) {
columnIndex = index
} else {
for (let index in this._columnHeights) {
if (this._columnHeights[index] < this._columnHeights[columnIndex]) {
columnIndex = index
}
}
}

const left = columnIndex * (columnWidth + gutterSize)
const top = this._columnHeights[columnIndex] || 0

this._columnHeights[columnIndex] = top + gutterSize + this._cache.getHeight(index)

return {
left,
top
}
}

_cellRenderer ({ index, key, parent, style }) {
const { list } = this.context
const { columnWidth } = this.state
Expand Down Expand Up @@ -186,15 +159,39 @@ export default class GridExample extends PureComponent {
)
}

_initOrResetPositioner () {
const {
columnWidth,
gutterSize
} = this.state

if (typeof this._cellPositioner === 'undefined') {
this._cellPositioner = createCellPositioner({
cellMeasurerCache: this._cache,
columnCount: this._columnCount,
columnWidth,
spacer: gutterSize
})
} else {
this._cellPositioner.reset({
columnCount: this._columnCount,
columnWidth,
spacer: gutterSize
})
}
}

_onResize (prevProps, prevState) {
this._columnHeights = {}
this._calculateColumnCount()
this._initOrResetPositioner()
this._masonry.recomputeCellPositions()
}

_renderMasonry ({ width }) {
this._width = width
this._calculateColumnCount()
this._initOrResetPositioner()

const { height } = this.state

Expand Down
30 changes: 6 additions & 24 deletions source/Masonry/Masonry.jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import { findDOMNode } from 'react-dom'
import { Simulate } from 'react-addons-test-utils'
import { render } from '../TestUtils'
import createCellPositionerUtil from './createCellPositioner'
import Masonry from './Masonry'
import { CellMeasurer, CellMeasurerCache } from '../CellMeasurer'

Expand Down Expand Up @@ -29,30 +30,11 @@ function createCellMeasurerCache (props = {}) {
}

function createCellPositioner (cache) {
const columnHeights = []
for (let i = 0; i < COLUMN_COUNT; i++) {
columnHeights[i] = 0
}

return function cellPositioner (index) {
// Super naive Masonry layout
let columnIndex = 0
for (let i = 1; i < columnHeights.length; i++) {
if (columnHeights[i] < columnHeights[columnIndex]) {
columnIndex = i
}
}

const left = columnIndex * CELL_SIZE_MULTIPLIER
const top = columnHeights[columnIndex] || 0

columnHeights[columnIndex] = top + cache.getHeight(index)

return {
left,
top
}
}
return createCellPositionerUtil({
cellMeasurerCache: cache,
columnCount: COLUMN_COUNT,
columnWidth: CELL_SIZE_MULTIPLIER
})
}

function createCellRenderer (cache, renderCallback) {
Expand Down
11 changes: 9 additions & 2 deletions source/Masonry/Masonry.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@ function noop () {

type KeyMapper = (index: number) => mixed;

export type CellMeasurerCache = {
defaultHeight: number,
defaultWidth: number,
getHeight: (index: number) => number,
getWidth: (index: number) => number
};

type CellRenderer = (params: {|
index: number,
isScrolling: boolean,
Expand All @@ -420,11 +427,11 @@ type Position = {
top: number
};

type Positioner = (index: number) => Position;
export type Positioner = (index: number) => Position;

type Props = {
cellCount: number,
cellMeasurerCache: mixed,
cellMeasurerCache: CellMeasurerCache,
cellPositioner: Positioner,
cellRenderer: CellRenderer,
className: ?string,
Expand Down
70 changes: 70 additions & 0 deletions source/Masonry/createCellPositioner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/** @flow */
import type {
CellMeasurerCache,
Positioner
} from './Masonry'

type createCellPositionerParams = {
cellMeasurerCache: CellMeasurerCache,
columnCount: number,
columnWidth: number,
spacer?: number
};

type resetParams = {
columnCount: number,
columnWidth: number,
spacer?: number
};

export default function createCellPositioner ({
cellMeasurerCache,
columnCount,
columnWidth,
spacer = 0
}: createCellPositionerParams): Positioner {
let columnHeights

initOrResetDerivedValues()

function cellPositioner (index) {
// Find the shortest column and use it.
let columnIndex = 0
for (let i = 1; i < columnHeights.length; i++) {
if (columnHeights[i] < columnHeights[columnIndex]) {
columnIndex = i
}
}

const left = columnIndex * (columnWidth + spacer)
const top = columnHeights[columnIndex] || 0

columnHeights[columnIndex] = top + cellMeasurerCache.getHeight(index)

return {
left,
top
}
}

function initOrResetDerivedValues (): void {
// Track the height of each column.
// Layout algorithm below always inserts into the shortest column.
columnHeights = []
for (let i = 0; i < columnCount; i++) {
columnHeights[i] = 0
}
}

function reset (params: resetParams): void {
columnCount = params.columnCount
columnWidth = params.columnWidth
spacer = params.spacer

initOrResetDerivedValues()
}

cellPositioner.reset = reset

return cellPositioner
}
1 change: 1 addition & 0 deletions source/Masonry/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @flow */
export default from './Masonry'
export Masonry from './Masonry'
export createCellPositioner from './createCellPositioner'
5 changes: 4 additions & 1 deletion source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export {
} from './Grid'
export { InfiniteLoader } from './InfiniteLoader'
export { List } from './List'
export { Masonry } from './Masonry'
export {
createCellPositioner as createMasonryCellPositioner,
Masonry
} from './Masonry'
export { MultiGrid } from './MultiGrid'
export { ScrollSync } from './ScrollSync'
export {
Expand Down

0 comments on commit f3f6867

Please sign in to comment.