From 0fd1ba5b8b1313c6f9a8d8f14bb39793f99b4516 Mon Sep 17 00:00:00 2001 From: Brandon Him Date: Mon, 9 Apr 2018 16:29:07 -0700 Subject: [PATCH] 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 --- __tests__/__snapshots__/masonry.test.js.snap | 2 + components/Masonry.js | 75 +++++++++++++------- example/App/Container/app.js | 54 +++++++++----- 3 files changed, 86 insertions(+), 45 deletions(-) diff --git a/__tests__/__snapshots__/masonry.test.js.snap b/__tests__/__snapshots__/masonry.test.js.snap index 85a8281..d79603b 100644 --- a/__tests__/__snapshots__/masonry.test.js.snap +++ b/__tests__/__snapshots__/masonry.test.js.snap @@ -23,6 +23,8 @@ exports[`SNAPSHOT: All functionality should match prev snapshot 1`] = ` } } enableEmptySections={true} + onEndReached={[Function]} + onEndReachedThreshold={25} refreshControl={undefined} removeClippedSubviews={false} renderRow={[Function]} diff --git a/components/Masonry.js b/components/Masonry.js index 209cb28..8753e5f 100644 --- a/components/Masonry.js +++ b/components/Masonry.js @@ -36,7 +36,9 @@ export default class Masonry extends Component { customImageProps: PropTypes.object, spacing: PropTypes.number, priority: PropTypes.string, - refreshControl: PropTypes.element + refreshControl: PropTypes.element, + onEndReached: PropTypes.func, + onEndReachedThreshold: PropTypes.number }; static defaultProps = { @@ -45,7 +47,10 @@ export default class Masonry extends Component { sorted: false, imageContainerStyle: {}, spacing: 1, - priority: 'order' + priority: 'order', + // no-op function + onEndReached: () => ({}), + onEndReachedThreshold: 25 }; constructor(props) { @@ -60,7 +65,8 @@ export default class Masonry extends Component { initialOrientation: true, _sortedData: [], _resolvedData: [], - _columnHeights: columnHeights + _columnHeights: columnHeights, + _uniqueCount: props.bricks.length }; // Assuming that rotation is binary (vertical|landscape) Dimensions.addEventListener('change', (window) => this.setState(state => ({ initialOrientation: !state.initialOrientation }))); @@ -76,24 +82,28 @@ export default class Masonry extends Component { // 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 => ({ _sortedData: [], _resolvedData: [], - _columnHeights: generateColumnHeights(nextProps.columns) + _columnHeights: generateColumnHeights(nextProps.columns), + _uniqueCount }), this.resolveBricks(nextProps)); } // We use the existing data and only resolve what is needed if (appendedData) { - this.resolveBricks({...nextProps, bricks: brickDiff}); - return; + const offSet = this.props.bricks.length; + this.setState({ + _uniqueCount + }, this.resolveBricks({...nextProps, bricks: brickDiff}, offSet)); } } - resolveBricks({ bricks, columns }) { + resolveBricks({ bricks, columns }, offSet = 0) { if (bricks.length === 0) { // clear and re-render this.setState(state => ({ @@ -105,7 +115,7 @@ export default class Masonry extends Component { // Issues arrise if state changes occur in the midst of a resolve bricks .map((brick, index) => assignObjectColumn(columns, index, brick)) - .map((brick, index) => assignObjectIndex(index, brick)) + .map((brick, index) => assignObjectIndex(offSet + index, brick)) .map(brick => resolveImage(brick)) .map(resolveTask => resolveTask.fork( (err) => console.warn('Image failed to load'), @@ -174,29 +184,40 @@ export default class Masonry extends Component { return dataCopy; }; + _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(); + } + } render() { return ( - this._setParentDimensions(event)}> - ( - - )} + this._setParentDimensions(event)}> + ( + + )} refreshControl={this.props.refreshControl} /> - + ); } }; diff --git a/example/App/Container/app.js b/example/App/Container/app.js index 3f9f01f..275a153 100644 --- a/example/App/Container/app.js +++ b/example/App/Container/app.js @@ -107,17 +107,27 @@ let data = [ } ]; -const addData = [ - { - uri: 'https://i.pinimg.com/736x/48/ee/51/48ee519a1768245ce273363f5bf05f30--kaylaitsines-dipping-sauces.jpg' - }, - { - uri: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGYfU5N8lsJepQyoAigiijX8bcdpahei_XqRWBzZLbxcsuqtiH' - }, - { - uri: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPL2GTXDuOzwuX5X7Mgwc3Vc9ZIhiMmZUhp3s1wg0oHPzSP7qC' - } -]; +const createBrick = uri => ({ uri }); + +const data1 = [ + 'https://i.pinimg.com/736x/48/ee/51/48ee519a1768245ce273363f5bf05f30--kaylaitsines-dipping-sauces.jpg', + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGYfU5N8lsJepQyoAigiijX8bcdpahei_XqRWBzZLbxcsuqtiH', + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPL2GTXDuOzwuX5X7Mgwc3Vc9ZIhiMmZUhp3s1wg0oHPzSP7qC', +].map(createBrick); + +const data2 = [ + 'https://i.pinimg.com/236x/ee/ea/61/eeea61b03a2de8216384ff1be9544b82--what-you-see-follow-me.jpg', + 'https://i.pinimg.com/originals/a0/30/87/a03087e36c083a665cd3392d2d59cf0b.jpg', + 'https://i.pinimg.com/736x/59/ae/8e/59ae8ee0e9e84aa234f80a345fa7f1c6--wedding-ceremony-wedding-menu.jpg' +].map(createBrick); + +const data3 = [ + 'https://i.pinimg.com/736x/6a/53/0a/6a530a49f764ce51a9742162f46c1e05--soul-food-pinterest.jpg', + 'https://i.pinimg.com/originals/a0/30/87/a03087e36c083a665cd3392d2d59cf0b.jpg', + 'https://img.buzzfeed.com/buzzfeed-static/static/2018-02/19/3/asset/buzzfeed-prod-fastlane-03/sub-buzz-12799-1519030318-7.jpg' +].map(createBrick); + +const appendData = [data1, data2, data3]; export default class example extends Component { constructor() { @@ -126,15 +136,23 @@ export default class example extends Component { this.state = { columns: 2, padding: 5, - data + data, + dataIndex: 0 }; } _addData = () => { - const appendedData = [...data, ...addData]; - this.setState({ - data: appendedData - }); + if (this.state.dataIndex < 3) { + this.setState(state => { + const addData = appendData[this.state.dataIndex]; + const appendedData = [...state.data, ...addData]; + + return { + data: appendedData, + dataIndex: state.dataIndex + 1 + }; + }); + } } render() { @@ -181,10 +199,10 @@ export default class example extends Component {