Skip to content

Commit

Permalink
Merging v0.5.0-alpha.x to Master (#112)
Browse files Browse the repository at this point in the history
* [In Progress] Adds Priority Option and Fixes Data Duplications (#86)

* Attempt to add in fixes suggested per other OSS modules

* Add semi colon

* First run for balance columns

* Rename function to findMinIndex

* Simplify array creation syntax

* Refactor balance

* Refactor priority and fix data update changes

* Update example to reference project dir

* Update masonry

* Minor stylistic changes, comment test

* Add updated snapshot

* Add lazy loading functionality (#87)

* Attempt to add in fixes suggested per other OSS modules

* Add semi colon

* First run for balance columns

* Rename function to findMinIndex

* Simplify array creation syntax

* Refactor balance

* Refactor priority and fix data update changes

* Update example to reference project dir

* Update masonry

* Minor stylistic changes, comment test

* Add updated snapshot

* Add append data

* Fix delay call reach

* Update snapshot for lazy load

* Set Alpha

* Add passable props to custom images (#96)

* Set alpha

* Revert

* passed data to customImageComponent (#94)

* Update package.json

* Fixed Bricks read only issue (#98)

I got an issue Bricks is read-only  because bricks is `const` at line 176 declared so it must be `let`

* Update package.json

* Update package.json (#109)
  • Loading branch information
brh55 authored Jun 4, 2019
1 parent 2aa583f commit 035c9b9
Show file tree
Hide file tree
Showing 11 changed files with 11,563 additions and 327 deletions.
4 changes: 4 additions & 0 deletions __tests__/__snapshots__/masonry.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ exports[`SNAPSHOT: All functionality should match prev snapshot 1`] = `
}
}
enableEmptySections={true}
onEndReached={[Function]}
onEndReachedThreshold={25}
refreshControl={undefined}
removeClippedSubviews={false}
renderRow={[Function]}
renderScrollComponent={[Function]}
scrollRenderAheadDistance={100}
>
<View />
</RCTScrollView>
Expand Down
51 changes: 30 additions & 21 deletions __tests__/masonry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Text, View, TouchableHighlight } from 'react-native';
import React from 'react';
// Note: test renderer must be required after react-native.
import Masonry, {
_insertIntoColumn,
assignObjectColumn,
assignObjectIndex,
_insertIntoColumn,
assignObjectColumn,
assignObjectIndex,
findMinIndex,
} from '../components/Masonry';
import renderer from 'react-test-renderer';

Expand Down Expand Up @@ -33,23 +34,31 @@ test('SNAPSHOT: All functionality should match prev snapshot', () => {
expect(tree).toMatchSnapshot();
});

test('PRIVATE FUNC: _insertIntoColumn sorts bricks according to index of bricks array and columns', () => {
const nColumns = 2;
const assignedBricks = minimumBricks
.map((brick, index) => assignObjectColumn(nColumns, index, brick))
.map((brick, index) => assignObjectIndex(index, brick));

const expectedData = [
[assignedBricks[0], assignedBricks[2]], //column 0
[assignedBricks[1], assignedBricks[3]] //column 1
];

//when
let expectedSorted = [];
assignedBricks.forEach((brick) => {
expectedSorted = _insertIntoColumn(brick, expectedSorted, true);
});
// We reinclude after further refactoring
// test('PRIVATE FUNC: _insertIntoColumn sorts bricks according to index of bricks array and columns', () => {
// const nColumns = 2;
// const assignedBricks = minimumBricks
// .map((brick, index) => assignObjectColumn(nColumns, index, brick))
// .map((brick, index) => assignObjectIndex(index, brick));

// const expectedData = [
// [assignedBricks[0], assignedBricks[2]], //column 0
// [assignedBricks[1], assignedBricks[3]] //column 1
// ];

// //when
// let expectedSorted = [];
// assignedBricks.forEach((brick) => {
// expectedSorted = _insertIntoColumn(brick, expectedSorted, true);
// });

// //then
// expect(expectedSorted).toEqual(expectedData);
// });

//then
expect(expectedSorted).toEqual(expectedData);
test('findShortestColumn returns shortest index', () => {
expect(findMinIndex([1,2,3,4,5,6])).toEqual(0);
expect(findMinIndex([100, 20000, 99, 44, 55])).toEqual(3);
expect(findMinIndex([99, 99, 99])).toEqual(0);
expect(findMinIndex([99, 8, 100, 1])).toEqual(3);
});
4 changes: 2 additions & 2 deletions components/Brick.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function _getImageTag (props, gutter = 0) {
width: props.width,
height: props.height,
marginTop: gutter,
...props.imageContainerStyle,
...props.imageContainerStyle
}
};

Expand All @@ -42,7 +42,7 @@ export function _getImageTag (props, gutter = 0) {
defaultProps={imageProps}
injectant={props.customImageComponent}
injectantProps={props.customImageProps} />
)
);
}

// _getTouchableUnit :: Image, Number -> TouchableTag
Expand Down
2 changes: 1 addition & 1 deletion components/Column.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class Column extends Component {

componentWillMount() {
this.setState({
images: this._resizeImages(this.props.data, this.props.parentDimensions, this.props.columns),
images: this._resizeImages(this.props.data, this.props.parentDimensions, this.props.columns)
});
}

Expand Down
205 changes: 131 additions & 74 deletions components/Masonry.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ import styles from '../styles/main';
// assignObjectColumn :: Number -> [Objects] -> [Objects]
export const assignObjectColumn = (nColumns, index, targetObject) => ({...targetObject, ...{ column: index % nColumns }});

// assignObjectIndex :: (Number, Object) -> Object
// Assigns an `index` property` from bricks={data}` for later sorting.
// assignObjectIndex :: (Number, Object) -> Object
export const assignObjectIndex = (index, targetObject) => ({...targetObject, ...{ index }});

// findMinIndex :: [Numbers] -> Number
export const findMinIndex = (srcArray) => srcArray.reduce((shortest, cValue, cIndex, cArray) => (cValue < cArray[shortest]) ? cIndex : shortest, 0);

// containMatchingUris :: ([brick], [brick]) -> Bool
const containMatchingUris = (r1, r2) => isEqual(r1.map(brick => brick.uri), r2.map(brick => brick.uri));
export const containMatchingUris = (r1, r2) => isEqual(r1.map(brick => brick.uri), r2.map(brick => brick.uri));

// Fills an array with 0's based on number count
// generateColumnsHeight :: Number -> Array [...0]
export const generateColumnHeights = count => Array(count).fill(0);

export default class Masonry extends Component {
static propTypes = {
Expand All @@ -28,81 +35,99 @@ export default class Masonry extends Component {
customImageComponent: PropTypes.func,
customImageProps: PropTypes.object,
spacing: PropTypes.number,
refreshControl: PropTypes.element
priority: PropTypes.string,
refreshControl: PropTypes.element,
onEndReached: PropTypes.func,
onEndReachedThreshold: PropTypes.number
};

static defaultProps = {
bricks: [],
columns: 2,
sorted: false,
imageContainerStyle: {},
spacing: 1
spacing: 1,
priority: 'order',
// no-op function
onEndReached: () => ({}),
onEndReachedThreshold: 25
};

constructor(props) {
super(props);
// Assuming users don't want duplicated images, if this is not the case we can always change the diff check
this.ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => !containMatchingUris(r1, r2) });
// This creates an array of [1..n] with values of 0, each index represent a column within the masonry
const columnHeights = generateColumnHeights(props.columns);
this.state = {
dataSource: this.ds.cloneWithRows([]),
dimensions: {},
initialOrientation: true,
_sortedData: [],
_resolvedData: []
_resolvedData: [],
_columnHeights: columnHeights,
_uniqueCount: props.bricks.length
};
// Assuming that rotation is binary (vertical|landscape)
Dimensions.addEventListener('change', (window) => this.setState(state => ({ initialOrientation: !state.initialOrientation })))
Dimensions.addEventListener('change', (window) => this.setState(state => ({ initialOrientation: !state.initialOrientation })));
}

componentDidMount() {
this.resolveBricks(this.props);
}

componentWillReceiveProps(nextProps) {
// Check if it's array and contains more than 1 item
if (!Array.isArray(nextProps.bricks) || nextProps.bricks.length === 0) {
const differentColumns = this.props.columns !== nextProps.columns;
const differentPriority = this.props.priority !== nextProps.priority;
// We use the difference in the passed in bricks to determine if user is appending or not
const brickDiff = differenceBy(nextProps.bricks, this.props.bricks, 'uri');
const appendedData = brickDiff.length !== nextProps.bricks.length;
const _uniqueCount = brickDiff.length + this.props.bricks.length;

// These intents would entail a complete re-render of the listview
if (differentColumns || differentPriority || !appendedData) {
this.setState(state => ({
dataSource: state.dataSource.cloneWithRows([])
}));
_sortedData: [],
_resolvedData: [],
_columnHeights: generateColumnHeights(nextProps.columns),
_uniqueCount
}), this.resolveBricks(nextProps));
}

const sameData = containMatchingUris(this.props.bricks, nextProps.bricks);
const differentColumns = this.props.columns !== nextProps.columns;

if (sameData && !differentColumns) {
// Only re-render a portion of the bricks
this.resolveBricks(nextProps, true);
} else {
this.resolveBricks(nextProps);
// We use the existing data and only resolve what is needed
if (appendedData) {
const offSet = this.props.bricks.length;
this.setState({
_uniqueCount
}, this.resolveBricks({...nextProps, bricks: brickDiff}, offSet));
}
}

resolveBricks({ bricks, columns }, partiallyCache = false) {
// Sort bricks and place them into their respectable columns
const sortedBricks = bricks
.map((brick, index) => assignObjectColumn(columns, index, brick))
.map((brick, index) => assignObjectIndex(index, brick));

// Do a difference check if these are new props
// to only resolve what is needed
const unresolvedBricks = (partiallyCache) ?
differenceBy(sortedBricks, this.state._resolvedData, 'uri') :
sortedBricks;
resolveBricks({ bricks, columns }, offSet = 0) {
if (bricks.length === 0) {
// clear and re-render
this.setState(state => ({
dataSource: state.dataSource.cloneWithRows([])
}));
}

unresolvedBricks
// Sort bricks and place them into their respectable columns
// Issues arrise if state changes occur in the midst of a resolve
bricks
.map((brick, index) => assignObjectColumn(columns, index, brick))
.map((brick, index) => assignObjectIndex(offSet + index, brick))
.map(brick => resolveImage(brick))
.map(resolveTask => resolveTask.fork(
(err) => console.warn('Image failed to load'),
(resolvedBrick) => {
this.setState(state => {
const sortedData = _insertIntoColumn(resolvedBrick, state._sortedData, this.props.sorted);

const sortedData = this._insertIntoColumn(resolvedBrick, state._sortedData);
return {
dataSource: state.dataSource.cloneWithRows(sortedData),
_sortedData: sortedData,
_resolvedData: [...state._resolvedData, resolvedBrick]
};
});;
});
}));
}

Expand All @@ -118,49 +143,81 @@ export default class Masonry extends Component {
});
}

render() {
return (
<View style={{flex: 1}} onLayout={(event) => this._setParentDimensions(event)}>
<ListView
contentContainerStyle={styles.masonry__container}
dataSource={this.state.dataSource}
enableEmptySections
renderRow={(data, sectionId, rowID) =>
<Column
data={data}
columns={this.props.columns}
parentDimensions={this.state.dimensions}
imageContainerStyle={this.props.imageContainerStyle}
customImageComponent={this.props.customImageComponent}
customImageProps={this.props.customImageProps}
spacing={this.props.spacing}
key={`RN-MASONRY-COLUMN-${rowID}`}/> }
refreshControl={this.props.refreshControl}
/>
</View>
)
}
};
_insertIntoColumn = (resolvedBrick, dataSet) => {
let dataCopy = dataSet.slice();
const priority = this.props.priority;
let columnIndex;

switch (priority) {
case 'balance':
// Best effort to balance but sometimes state changes may have delays when performing calculation
columnIndex = findMinIndex(this.state._columnHeights);
const heightsCopy = this.state._columnHeights.slice();
const newColumnHeights = heightsCopy[columnIndex] + resolvedBrick.dimensions.height;
heightsCopy[columnIndex] = newColumnHeights;
this.setState({
_columnHeights: heightsCopy
});
break;
case 'order':
default:
columnIndex = resolvedBrick.column;
break;
}

const column = dataSet[columnIndex];
const sorted = this.props.sorted;

if (column) {
// Append to existing "row"/"column"
let bricks = [...column, resolvedBrick];
if (sorted) {
// Sort bricks according to the index of their original array position
bricks = bricks.sort((a, b) => (a.index < b.index) ? -1 : 1);
}
dataCopy[columnIndex] = bricks;
} else {
// Pass it as a new "row" for the data source
dataCopy = [...dataCopy, [resolvedBrick]];
}

return dataCopy;
};

// Returns a copy of the dataSet with resolvedBrick in correct place
// (resolvedBrick, dataSetA, bool) -> dataSetB
export function _insertIntoColumn (resolvedBrick, dataSet, sorted) {
let dataCopy = dataSet.slice();
const columnIndex = resolvedBrick.column;
const column = dataSet[columnIndex];

if (column) {
// Append to existing "row"/"column"
const bricks = [...column, resolvedBrick];
if (sorted) {
// Sort bricks according to the index of their original array position
bricks = bricks.sort((a, b) => (a.index < b.index) ? -1 : 1);
_delayCallEndReach = () => {
const sortedData = this.state._sortedData;
const sortedLength = sortedData.reduce((acc, cv) => cv.length + acc, 0);
// Limit the invokes to only when the masonry has
// fully loaded all of the content to ensure user fully reaches the end
if (sortedLength === this.state._uniqueCount) {
this.props.onEndReached();
}
dataCopy[columnIndex] = bricks;
} else {
// Pass it as a new "row" for the data source
dataCopy = [...dataCopy, [resolvedBrick]];
}

return dataCopy;
render() {
return (
<View style={{flex: 1}} onLayout={(event) => this._setParentDimensions(event)}>
<ListView
contentContainerStyle={styles.masonry__container}
dataSource={this.state.dataSource}
enableEmptySections
scrollRenderAheadDistance={100}
removeClippedSubviews={false}
onEndReached={this._delayCallEndReach}
onEndReachedThreshold={this.props.onEndReachedThreshold}
renderRow={(data, sectionId, rowID) => (
<Column
data={data}
columns={this.props.columns}
parentDimensions={this.state.dimensions}
imageContainerStyle={this.props.imageContainerStyle}
customImageComponent={this.props.customImageComponent}
customImageProps={this.props.customImageProps}
spacing={this.props.spacing}
key={`RN-MASONRY-COLUMN-${rowID}`} />
)}
refreshControl={this.props.refreshControl} />
</View>
);
}
};
Loading

0 comments on commit 035c9b9

Please sign in to comment.