├── example ├── .watchmanconfig ├── .gitignore ├── assets │ └── icons │ │ └── app-icon.png ├── .babelrc ├── src │ ├── index.js │ ├── util.js │ ├── InfiniteScroll-resize.js │ ├── InfiniteScroll-basic.js │ ├── InfiniteScroll-scroll.js │ └── InfiniteScroll-grid.js ├── app.json ├── package.json └── App.js ├── .gitignore ├── src ├── res │ ├── index.js │ ├── Indicator.js │ ├── css.js │ └── Error.js ├── css.js └── InfiniteScroll.js ├── index.js ├── package.json ├── LICENSE └── README.md /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | .DS_Store 4 | .expo/* -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | -------------------------------------------------------------------------------- /example/assets/icons/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBuzzArt/react-native-infinite/HEAD/example/assets/icons/app-icon.png -------------------------------------------------------------------------------- /src/res/index.js: -------------------------------------------------------------------------------- 1 | import Indicator from './Indicator'; 2 | import Error from './Error'; 3 | 4 | 5 | export { 6 | Indicator, 7 | Error, 8 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import InfiniteScroll from './src/InfiniteScroll'; 2 | import * as res from './src/res'; 3 | 4 | 5 | module.exports = { 6 | InfiniteScroll, 7 | res 8 | }; -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/css.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | 4 | export default StyleSheet.create({ 5 | 6 | viewport: { 7 | overflow: 'hidden', 8 | }, 9 | viewport_fullHeight: { 10 | flex: 1, 11 | }, 12 | 13 | list: {}, 14 | 15 | block: {}, 16 | 17 | footer: {}, 18 | footer__loading: {}, 19 | 20 | statusBar: {}, 21 | }); -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import ExampleBasic from './InfiniteScroll-basic'; 2 | import ExampleResize from './InfiniteScroll-resize'; 3 | import ExampleScroll from './InfiniteScroll-scroll'; 4 | import ExampleGrid from './InfiniteScroll-grid'; 5 | 6 | 7 | export { 8 | ExampleBasic, 9 | ExampleResize, 10 | ExampleScroll, 11 | ExampleGrid, 12 | }; -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-infinite", 4 | "description": "Infinite index loader for react native", 5 | "slug": "react-native-infinite", 6 | "privacy": "public", 7 | "sdkVersion": "19.0.0", 8 | "version": "1.0.0", 9 | "orientation": "default", 10 | "primaryColor": "#cccccc", 11 | "icon": "./assets/icons/app-icon.png", 12 | "packagerOpts": { 13 | "assetExts": ["ttf", "mp4"] 14 | }, 15 | "ios": { 16 | "supportsTablet": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-infinite-example", 3 | "version": "0.0.0", 4 | "description": "Hello Expo!", 5 | "author": null, 6 | "private": true, 7 | "main": "node_modules/expo/AppEntry.js", 8 | "dependencies": { 9 | "babel-preset-expo": "^3.0.0", 10 | "expo": "^19.0.0", 11 | "react": "16.0.0-alpha.12", 12 | "react-native": "https://github.com/expo/react-native/archive/sdk-19.0.0.tar.gz", 13 | "react-native-infinite": "^1.1.2", 14 | "react-navigation": "^1.0.0-beta.11" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/res/Indicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, ActivityIndicator, ViewPropTypes } from 'react-native'; 3 | 4 | import css from './css'; 5 | 6 | 7 | export default class Indicator extends React.Component { 8 | 9 | static propTypes = { 10 | style: ViewPropTypes.style, 11 | }; 12 | static defaultProps = { 13 | style: null 14 | }; 15 | 16 | render() { 17 | const { props } = this; 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/res/css.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | 4 | export default StyleSheet.create({ 5 | 6 | // ./Indicator.js 7 | loading: { 8 | paddingVertical: 20, 9 | }, 10 | loading__body: { 11 | 12 | }, 13 | 14 | // ./Error.js 15 | error: { 16 | flex: 1, 17 | alignItems: 'center', 18 | justifyContent: 'center', 19 | }, 20 | error__message: { 21 | marginBottom: 20, 22 | fontSize: 14, 23 | color: '#222', 24 | }, 25 | error__reload: { 26 | paddingHorizontal: 20, 27 | paddingTop: 10, 28 | paddingBottom: 10, 29 | backgroundColor: '#f1f1f1', 30 | }, 31 | error__reloadText: { 32 | fontSize: 12, 33 | }, 34 | 35 | }); -------------------------------------------------------------------------------- /example/src/util.js: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | 4 | export function sleep(delay=3000) { 5 | clearTimeout(GLOBAL.appTimer); 6 | 7 | return new Promise(resolve => { 8 | GLOBAL.appTimer = setTimeout(resolve, delay); 9 | }); 10 | } 11 | 12 | 13 | export class ResizeEvent { 14 | 15 | constructor() { 16 | 17 | this._onChange = this._change.bind(this); 18 | 19 | Dimensions.addEventListener('change', this._onChange); 20 | } 21 | 22 | _change(e) { 23 | this.onChange(e); 24 | } 25 | 26 | onChange() { 27 | console.log('on change inside'); 28 | } 29 | 30 | destroy() { 31 | Dimensions.removeEventListener('change', this._onChange); 32 | } 33 | } -------------------------------------------------------------------------------- /src/res/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { View, Text, TouchableOpacity } from 'react-native'; 4 | 5 | import css from './css'; 6 | 7 | 8 | export default class Error extends React.Component { 9 | 10 | static propTypes = { 11 | message: PropTypes.string, 12 | }; 13 | static defaultProps = { 14 | message: 'error message', 15 | onReload: null, 16 | }; 17 | 18 | render() { 19 | const { props } = this; 20 | 21 | return ( 22 | 23 | {props.message} 24 | {props.onReload && ( 25 | 26 | Reload 27 | 28 | )} 29 | 30 | ); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-infinite", 3 | "version": "1.1.8", 4 | "description": "Infinite list for react native", 5 | "private": false, 6 | "main": "index.js", 7 | "scripts": { 8 | "version-patch": "npm version patch", 9 | "publish": "npm publish" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/BBuzzArt/react-native-infinite.git" 14 | }, 15 | "author": "BBuzzArt (https://bbuzzart.com)", 16 | "contributors": [ 17 | "redgoose (http://redgoose.me)" 18 | ], 19 | "license": "MIT", 20 | "keywords": [ 21 | "react native", 22 | "react", 23 | "list", 24 | "infinite", 25 | "item list", 26 | "block list", 27 | "index" 28 | ], 29 | "bugs": { 30 | "url": "https://github.com/BBuzzArt/react-native-infinite/issues" 31 | }, 32 | "homepage": "https://github.com/BBuzzArt/react-native-infinite#readme", 33 | "dependencies": {}, 34 | "directories": { 35 | "example": "example" 36 | }, 37 | "devDependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 BBuzzArt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/src/InfiniteScroll-resize.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, Dimensions } from 'react-native'; 3 | import { InfiniteScroll } from 'react-native-infinite'; 4 | 5 | import * as util from './util'; 6 | 7 | 8 | const items = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 9 | const css = StyleSheet.create({ 10 | viewport: { 11 | flex: 1, 12 | }, 13 | item: { 14 | backgroundColor: '#d7f7ff', 15 | alignItems: 'center', 16 | justifyContent: 'center', 17 | }, 18 | item__text: { 19 | 20 | }, 21 | }); 22 | 23 | 24 | export default class InfiniteScrollExampleResize extends React.Component { 25 | 26 | constructor() { 27 | super(); 28 | 29 | this.state = { 30 | column: this.getColumn(), 31 | blank: false, 32 | }; 33 | 34 | this._infiniteScroll = null; 35 | this.binds = { 36 | renderRow: this.renderRow.bind(this), 37 | onResize: this.onResize.bind(this), 38 | }; 39 | } 40 | 41 | componentDidMount() { 42 | this.resizeEvent = new util.ResizeEvent(); 43 | this.resizeEvent.onChange = this.binds.onResize; 44 | } 45 | 46 | componentWillUnmount() { 47 | this.resizeEvent.destroy(); 48 | } 49 | 50 | getColumn() { 51 | return (Dimensions.get('window').width > 640) ? 4 : 2; 52 | } 53 | 54 | async onResize() { 55 | const { state } = this; 56 | const column = this.getColumn(); 57 | 58 | if (column === state.column) { 59 | this._infiniteScroll.forceUpdate(); 60 | return; 61 | } 62 | 63 | await this.setState({ blank: true, column }); 64 | this.setState({ blank: false }); 65 | } 66 | 67 | renderRow({ item, index, size }) { 68 | return ( 69 | 73 | box{index} 74 | 75 | ); 76 | } 77 | 78 | render() { 79 | const { props, state } = this; 80 | 81 | return ( 82 | 83 | {!state.blank ? ( 84 | { this._infiniteScroll = r; }} 86 | items={items} 87 | useScrollEvent={false} 88 | useRefresh={false} 89 | column={state.column} 90 | innerMargin={5} 91 | outerMargin={0} 92 | type="end" 93 | renderRow={this.binds.renderRow} 94 | /> 95 | ) : null} 96 | 97 | ); 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View, TouchableHighlight, ScrollView } from 'react-native'; 3 | import { StackNavigator } from 'react-navigation'; 4 | 5 | import * as src from './src'; 6 | 7 | 8 | class App extends React.Component { 9 | 10 | render() { 11 | const { props } = this; 12 | 13 | return ( 14 | 15 | 16 | props.navigation.navigate('ExampleBasic')}> 20 | 21 | Basic / load items to infinity 22 | 23 | 24 | props.navigation.navigate('ExampleResize')}> 28 | 29 | Resize / resize screen event 30 | 31 | 32 | props.navigation.navigate('ExampleScroll')}> 36 | 37 | Scroll / scroll event method 38 | 39 | 40 | props.navigation.navigate('ExampleGrid')}> 44 | 45 | Grid / random size blocks 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | } 53 | 54 | 55 | const css = StyleSheet.create({ 56 | viewport: { 57 | flex: 1, 58 | }, 59 | index: {}, 60 | item: { 61 | paddingVertical: 15, 62 | paddingHorizontal: 15, 63 | borderBottomWidth: StyleSheet.hairlineWidth, 64 | borderBottomColor: 'rgba(0,0,0,.2)', 65 | }, 66 | item__text: { 67 | fontWeight: '600', 68 | fontSize: 16, 69 | color: '#324dff', 70 | }, 71 | cardStyle: { 72 | backgroundColor: '#fff' 73 | }, 74 | headerStyle: { 75 | backgroundColor: '#fff', 76 | borderBottomWidth: StyleSheet.hairlineWidth, 77 | borderBottomColor: '#aaa', 78 | }, 79 | }); 80 | 81 | export default StackNavigator({ 82 | Home: { screen: App, navigationOptions: { title: 'Demos', headerStyle: css.headerStyle } }, 83 | ExampleBasic: { screen: src.ExampleBasic, navigationOptions: { title: 'Basic', headerStyle: css.headerStyle } }, 84 | ExampleResize: { screen: src.ExampleResize, navigationOptions: { title: 'Resize', headerStyle: css.headerStyle } }, 85 | ExampleScroll: { screen: src.ExampleScroll, navigationOptions: { title: 'Scroll', headerStyle: css.headerStyle } }, 86 | ExampleGrid: { screen: src.ExampleGrid, navigationOptions: { title: 'Grid', headerStyle: css.headerStyle } }, 87 | 88 | }, { 89 | cardStyle: css.cardStyle, 90 | }); -------------------------------------------------------------------------------- /example/src/InfiniteScroll-basic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | import { InfiniteScroll } from 'react-native-infinite'; 4 | 5 | import * as util from './util'; 6 | 7 | 8 | const items = [ 9 | { label: 'red' }, 10 | { label: 'orange' }, 11 | { label: 'yellow' }, 12 | { label: 'green' }, 13 | { label: 'blue' }, 14 | { label: 'darkblue' }, 15 | { label: 'violet' }, 16 | ]; 17 | const css = StyleSheet.create({ 18 | viewport: { 19 | flex: 1, 20 | }, 21 | scroll: {}, 22 | scrollList: {}, 23 | scrollRow: {}, 24 | block: { 25 | flex: 1, 26 | }, 27 | block__wrap: { 28 | flex: 1, 29 | alignItems: 'center', 30 | justifyContent: 'center', 31 | }, 32 | block__text: {}, 33 | }); 34 | 35 | 36 | export default class InfiniteScrollExampleBasic extends React.Component { 37 | 38 | constructor(props) { 39 | super(); 40 | 41 | this._infiniteScroll = null; 42 | this.isMount = false; 43 | 44 | this.state = { 45 | items: items, 46 | type: 'ready', 47 | }; 48 | } 49 | 50 | componentDidMount() { 51 | this.isMount = true; 52 | } 53 | 54 | componentWillUnmount() { 55 | this.isMount = false; 56 | } 57 | 58 | async load(type) { 59 | const { props, state } = this; 60 | 61 | switch(type) { 62 | case 'more': 63 | await this.setState({ type: 'loading' }); 64 | await util.sleep(500); 65 | if (!this.isMount) return; 66 | this.setState({ 67 | type: 'ready', 68 | items: [ 69 | ...state.items, 70 | ...items, 71 | ] 72 | }); 73 | break; 74 | 75 | case 'refresh': 76 | await this.setState({ type: 'refresh' }); 77 | await util.sleep(1000); 78 | if (!this.isMount) return; 79 | this.setState({ 80 | type: state.type === 'end' ? 'end' : 'ready', 81 | items: items, 82 | }); 83 | break; 84 | } 85 | } 86 | 87 | renderRow({ item, index, size }) { 88 | return ( 89 | 93 | 94 | {item.label} 95 | 96 | 97 | ); 98 | } 99 | 100 | render() { 101 | const { props, state } = this; 102 | 103 | return ( 104 | 105 | { this._infiniteScroll = r; }} 107 | items={state.items} 108 | itemHeight={60} 109 | column={2} 110 | innerMargin={[5,1]} 111 | outerMargin={[5,5]} 112 | type={state.type} 113 | load={(type) => this.load(type)} 114 | renderRow={(res) => this.renderRow(res)} 115 | renderHeader={() => Header component} 116 | renderFooter={() => Footer component} 117 | style={css.scroll} 118 | styleList={css.scrollList} 119 | styleRow={css.scrollRow}/> 120 | 121 | ); 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /example/src/InfiniteScroll-scroll.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; 3 | import { InfiniteScroll } from 'react-native-infinite'; 4 | 5 | import * as util from './util'; 6 | 7 | 8 | const items = [ 9 | { label: 'red' }, 10 | { label: 'orange' }, 11 | { label: 'yellow' }, 12 | { label: 'green' }, 13 | { label: 'blue' }, 14 | { label: 'darkblue' }, 15 | { label: 'violet' }, 16 | ]; 17 | const css = StyleSheet.create({ 18 | viewport: { 19 | flex: 1, 20 | }, 21 | scroll: {}, 22 | scrollList: {}, 23 | scrollRow: {}, 24 | block: { 25 | flex: 1, 26 | }, 27 | block__wrap: { 28 | flex: 1, 29 | alignItems: 'center', 30 | justifyContent: 'center', 31 | }, 32 | block__text: {}, 33 | nav: { 34 | flexDirection: 'row', 35 | paddingVertical: 10, 36 | paddingHorizontal: 5, 37 | borderTopWidth: StyleSheet.hairlineWidth, 38 | borderTopColor: 'rgba(0,0,0,.2)', 39 | backgroundColor: '#f9f9f9', 40 | }, 41 | button: { 42 | flex: 1, 43 | alignItems: 'center', 44 | justifyContent: 'center', 45 | paddingVertical: 10, 46 | marginHorizontal: 5, 47 | backgroundColor: '#999' 48 | }, 49 | button__text: { 50 | fontSize: 12, 51 | color: '#fff', 52 | fontWeight: '600', 53 | }, 54 | }); 55 | 56 | 57 | export default class InfiniteScrollExampleBasic extends React.Component { 58 | 59 | constructor(props) { 60 | super(); 61 | 62 | this._infiniteScroll = null; 63 | this.state = { 64 | items: items, 65 | type: 'ready', 66 | }; 67 | } 68 | 69 | componentDidMount() { 70 | this.isMount = true; 71 | } 72 | 73 | componentWillUnmount() { 74 | this.isMount = false; 75 | } 76 | 77 | async load(type) { 78 | const { props, state } = this; 79 | 80 | switch(type) { 81 | case 'more': 82 | await this.setState({ type: 'loading' }); 83 | await util.sleep(500); 84 | if (!this.isMount) return; 85 | this.setState({ 86 | type: 'ready', 87 | items: [ 88 | ...state.items, 89 | ...items, 90 | ] 91 | }); 92 | break; 93 | 94 | case 'refresh': 95 | await this.setState({ type: 'refresh' }); 96 | await util.sleep(1000); 97 | if (!this.isMount) return; 98 | this.setState({ 99 | type: state.type === 'end' ? 'end' : 'ready', 100 | items: items, 101 | }); 102 | break; 103 | } 104 | } 105 | 106 | renderRow({ item, index, size }) { 107 | return ( 108 | 112 | 113 | {item.label} 114 | 115 | 116 | ); 117 | } 118 | 119 | render() { 120 | const { props, state } = this; 121 | 122 | return ( 123 | 124 | { this._infiniteScroll = r; }} 126 | items={state.items} 127 | itemHeight={100} 128 | column={2} 129 | innerMargin={10} 130 | outerMargin={10} 131 | type={state.type} 132 | load={(type) => this.load(type)} 133 | renderRow={(res) => this.renderRow(res)} 134 | style={css.scroll} 135 | styleList={css.scrollList} 136 | styleRow={css.scrollRow}/> 137 | 138 | { 140 | this._infiniteScroll.list.scrollToOffset({ offset: 0 }); 141 | }} 142 | style={css.button}> 143 | Scroll to top 144 | 145 | { 147 | this._infiniteScroll.list.scrollToIndex({ index: 4 }); 148 | }} 149 | style={css.button}> 150 | Scroll to 4 line 151 | 152 | 153 | 154 | ); 155 | } 156 | 157 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-infinite 2 | 3 | React Native infinite는 쉽게 목록형 데이터를 표현하는 래퍼(Wrapper)라고 할 수 있습니다. 4 | 5 | 6 | ## Features 7 | 8 | - Flatlist 컴포넌트 사용 9 | - 당겨서 새로고침 지원 10 | - 더 불러오기 지원 11 | - redux와 함께 사용하기 적합한 목록 12 | 13 | 14 | ## Demo 15 | 16 | [Expo](https://expo.io) 앱을 통하여 데모를 확인해볼 수 있습니다. 데모는 다음링크를 참고하세요. 17 | 18 | https://expo.io/@bbuzzart/react-native-infinite 19 | 20 | 21 | ## Installation 22 | cli로 설치할 프로젝트에서 다음과 같은 명령을 실행하여 디펜더시를 추가합니다. 23 | 24 | ### npm 25 | `npm install --save react-native-infinite` 26 | 27 | ### yarn 28 | `yarn add react-native-infinite` 29 | 30 | 31 | ## Usage 32 | 다음 소스코드는 가장 기본적인 형태의 예제입니다. 33 | 34 | ``` 35 | import { InfiniteScroll } from 'react-native-infinite'; 36 | 37 | ( {item.name} )} 44 | /> 45 | ``` 46 | 47 | 컴포넌트를 활용한 예제는 다음 소스코드 링크를 참고해주세요. 48 | - [InfiniteScroll-basic](https://github.com/BBuzzArt/react-native-infinite/blob/master/example/src/InfiniteScroll-basic.js) 49 | - [InfiniteScroll-resize](https://github.com/BBuzzArt/react-native-infinite/blob/master/example/src/InfiniteScroll-resize.js) 50 | - [InfiniteScroll-scroll](https://github.com/BBuzzArt/react-native-infinite/blob/master/example/src/InfiniteScroll-scroll.js) 51 | - [InfiniteScroll-grid](https://github.com/BBuzzArt/react-native-infinite/blob/master/example/src/InfiniteScroll-grid.js) 52 | 53 | 54 | ## Properties 55 | 56 | ### basic 57 | 58 | | Name | default | Type | Description | 59 | | :--- | :------ | :--- | :---------- | 60 | | items | null | `array` | 목록이 되는 배열 형태의 데이터를 넣습니다. 이 prop은 *필수값*입니다. | 61 | | width | 'auto' | `string\|number` | 목록 영역의 가로사이즈 | 62 | | itemHeight | null | `number` | 아이템의 높이 | 63 | 64 | ### use 65 | 66 | | Name | default | Type | Description | 67 | | :--- | :------ | :--- | :---------- | 68 | | useScrollEvent | true | `boolean` | 이미지 더보기 기능을 하는 스크롤 이벤트 사용 | 69 | | useRefresh | true | `boolean` | 목록을 아래로 당기면서 새로고침 이벤트 사용 | 70 | | useFullHeight | true | `boolean` | 목록을 전체화면으로 사용 | 71 | | useDebug | false | `boolean` | debug모드 사용 | 72 | 73 | ### options 74 | 75 | | Name | default | Params | Type | Description | 76 | | :--- | :------ | :----- | :--- | :---------- | 77 | | column | 1 | | `number` | 컬럼 수 | 78 | | innerMargin | `[0,0]` | | `number\|array` | 요소 사이의 간격. ex) `[가로,세로]` | 79 | | outerMargin | `[0,0]` | | `number\|array` | 목록 외곽의 간격. ex) `[가로,세로]` | 80 | | removeClippedSubviews | true | | `boolean` | 안보이는 요소는 언마운트할지에 대한 여부 | 81 | | endReachedPosition | 2 | | `number` | 요소 더 불러오기 이벤트 시작하는 지점 | 82 | | pageSize | 20 | | `number` | 한번에 표시하는 요소 갯수 | 83 | | keyExtractor | null | | `string` | 요소를 구분하는 key값 정의 | 84 | | type | `'end'` | | `string` | 목록의 상태 (`loading`:로딩중, `refresh`:새로고침 중, `ready`:대기중, `end`:더이상 불러올것이 없는상태) | 85 | | load | `function()` | `type` | `function` | 새로고침하거나 더 불러오기할때 실행되는 이벤트. `type`이라는 현재 목록 상태를 참고하여 목록을 직업 갱신할 수 있습니다. `type`은 `props.type`값과 같은 내용입니다. | 86 | | getItemLayout | null | `{data, index}` | `object` | 블럭의 사이즈를 정의합니다. | 87 | 88 | ### render 89 | 90 | | Name | default | Params | Type | Description | 91 | | :--- | :------ | :----- | :--- | :---------- | 92 | | renderRow | null | `{item, index, size}` | `function` | 요소 하나를 렌더하는 컴포넌트. 파라메터를 이용하여 컴포넌트를 return을 통하여 출력합니다. | 93 | | renderHeader | null | | `function` | 목록의 상단 컴포넌트 | 94 | | renderFooter | null | | `function` | 목록의 하단을 컴포넌트 | 95 | | renderError | `` | | `function` | 오류가 났을때 출력하는 컴포넌트 | 96 | | renderNotFound | `` | | `function` | 아이템이 없을때 출력하는 컴포넌트 | 97 | 98 | ### style 99 | 100 | | Name | default | Type | Description | 101 | | :--- | :------ | :--- | :---------- | 102 | | style | null | `style` | 컴포넌트의 가장 바깥의 영역 | 103 | | styleList | null | `style` | 목록 | 104 | | styleRow | null | `style` | 목록에서 하나의 줄 | 105 | | styleBlock | null | `style` | 목록에서 하나의 요소 | 106 | | styleHeader | null | `style` | 헤더영역 | 107 | | styleFooter | null | `style` | 푸터영역 | 108 | 109 | 110 | ## API 111 | 112 | 먼저 컴포넌트로 접근할 수 있도록 인스턴스 객체로 담아둡니다. 113 | 다음과 같이 `this.infiniteScrollRef`로 컴포넌트에 접근할 수 있습니다. 114 | 115 | ``` 116 | import React from 'react'; 117 | import { InfiniteScroll } from 'react-native-infinite'; 118 | 119 | export default class Foo extends React.Component { 120 | constructor(props) { 121 | this.infiniteScrollRef = null; 122 | } 123 | 124 | render() { 125 | return ( 126 | { this.infiniteScrollRef = r; }}/> 127 | ); 128 | } 129 | } 130 | ``` 131 | 132 | ### FlatList 133 | 134 | `FlatList`를 사용하여 어떤 액션을 사용하려면 `this.infiniteScrollRef.list` 객체로 접근하여 `FlatList`의 메서드를 사용할 수 있습니다. 135 | 136 | _example)_ 137 | 138 | ``` 139 | // 가장 아래쪽으로 스크롤 이동 140 | this.infiniteScrollRef.list.scrollToEnd(); 141 | 142 | // offset값의 위치로 스크롤 이동 143 | this.infiniteScrollRef.list.scrollToOffset({ 144 | offset: 20, 145 | }); 146 | ``` 147 | 148 | ### scrollToOffset 149 | 원하는 위치로 스크롤을 이동합니다. 150 | 151 | ``` 152 | /** 153 | * @param {Object} options 154 | * @param {int} options.offset : 이동하려는 위치 offset 값 155 | * @param {int} options.animated : 애니메이션 사용유무 156 | */ 157 | this.infiniteScrollRef.list.scrollToOffset({ 158 | offset: 0, 159 | animated: true 160 | }); 161 | ``` 162 | 163 | ### reRender 164 | 컬럼을 변경하게 되면 `FlatList`에서 오류가 발생됩니다. state로 컬럼 변경이 불가능해 보입니다. `reRender()`메서드를 사용하면 `FlatList` 컴포넌트를 삭제하고 다시 마운트를 합니다. 165 | 166 | > #### 주의 167 | > 168 | > 컴포넌트가 순간적으로 삭제되기 때문에 스크롤 위치가 이동할 수 있습니다. 169 | 170 | ``` 171 | this.infiniteScrollRef.reRender(); 172 | ``` 173 | 174 | 175 | ---- 176 | 177 | 178 | Powered by [BBuzzArt](http://bbuzzart.com) 179 | 180 | - iOS: https://itunes.apple.com/us/app/bbuzzart-new-art-in-your-hand/id868618986 181 | - android: https://play.google.com/store/apps/details?id=net.bbuzzart.android 182 | -------------------------------------------------------------------------------- /example/src/InfiniteScroll-grid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, Image, Dimensions, StyleSheet, ActivityIndicator } from 'react-native'; 3 | import { InfiniteScroll } from 'react-native-infinite'; 4 | 5 | import * as util from './util'; 6 | 7 | 8 | const patterns = [ 9 | 'oo##--##', 'oo||--||', 'o##|o##|', 'o|##o|##', 'o|--o|--', 10 | 'o--|o--|', 'o--|--o|', '##oo##--', '##o|##o|', '########', 11 | '##|o##|o', '##||##||', '##--##oo', '##--##--', '|oo||--|', 12 | '|o##|o##', '|o--|o--', '|o--|--o', '|##o|##o', '|##||##|', 13 | '||oo||--', '||##||##', '||--||oo', '||--||--', '|--o|o--', 14 | '|--o|--o', '|--||oo|', '|--||--|', '--o|o--|', '--o|--o|', 15 | '--##oo##', '--##--##', '--|o--|o', '--||oo||', '--||--||' 16 | ].map((str) => { return str.split(''); }); 17 | const IMAGES = [ 18 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/47707_20170627080238_thumb_S', 19 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/53003_20170715014116_thumb_S', 20 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/53228_20170626065658_thumb_S', 21 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/43529_20170619035318_thumb_S', 22 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/40187_20170819025619_thumb_S', 23 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/1606_20150828052813_thumb_S', 24 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/31757_20170807092734_thumb_S', 25 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/25657_20170817044526_thumb_S', 26 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/21548_20170731103716_thumb_S', 27 | 'https://djo93u9c0domr.cloudfront.net/attachment-thumbs/7656_20160127004356_thumb_S', 28 | ]; 29 | const MARGIN = 5; 30 | const SIZE = (Dimensions.get('window').width - (MARGIN * 3)) / 4; 31 | 32 | const css = StyleSheet.create({ 33 | loading: { 34 | flex: 1, 35 | alignItems: 'center', 36 | justifyContent: 'center', 37 | }, 38 | loading__text: { 39 | marginTop: 8, 40 | fontSize: 11, 41 | color: '#111', 42 | }, 43 | }); 44 | 45 | 46 | export default class Grid extends React.Component { 47 | 48 | constructor() 49 | { 50 | super(); 51 | 52 | this.state = { 53 | articles: [], 54 | type: 'ready', 55 | }; 56 | this.size = (Dimensions.get('window').width / 4); 57 | } 58 | 59 | async componentDidMount() 60 | { 61 | await util.sleep(1000); 62 | 63 | this.setState({ 64 | articles: this.makeImages(getImages(IMAGES, 30)) 65 | }); 66 | } 67 | 68 | makeImages(src) 69 | { 70 | let copy_src = Object.assign([], src); 71 | let result = []; 72 | 73 | function get() 74 | { 75 | return copy_src.pop(); 76 | } 77 | 78 | function display(src, x, y, width, height) 79 | { 80 | return { 81 | src: src ? src : null, 82 | x: x * SIZE + (x) * MARGIN, 83 | y: y * SIZE + (y) * MARGIN, 84 | width: width * SIZE + (width - 1) * MARGIN, 85 | height: height * SIZE + (height - 1) * MARGIN, 86 | }; 87 | } 88 | 89 | function group(block) 90 | { 91 | let re = []; 92 | 93 | for (let i=0; i<8; i++) 94 | { 95 | switch (block[i]) 96 | { 97 | case 'o': 98 | re.push(display(get(), i % 4, Math.floor(i / 4), 1, 1)); 99 | break; 100 | case '#': 101 | re.push(display(get(), i % 4, Math.floor(i / 4), 2, 2)); 102 | block[i] = block[i + 1] = block[i + 4] = block[i + 5] = 'x'; 103 | break; 104 | case '|': 105 | re.push(display(get(), i % 4, Math.floor(i / 4), 1, 2)); 106 | block[i] = block[i + 4] = 'x'; 107 | break; 108 | case '-': 109 | re.push(display(get(), i % 4, Math.floor(i / 4), 2, 1)); 110 | block[i] = block[i + 1] = 'x'; 111 | break; 112 | } 113 | } 114 | result.push(re); 115 | } 116 | 117 | while(copy_src.length) 118 | { 119 | group(patterns[patterns.length * Math.random() | 0].concat()); 120 | } 121 | 122 | return result; 123 | } 124 | 125 | renderItem({ item, index }) 126 | { 127 | const { props, state } = this; 128 | 129 | return ( 130 | 134 | {item.map((block, key) => { 135 | let style = { 136 | position: 'absolute', 137 | left: block.x, 138 | top: block.y, 139 | backgroundColor: '#eee', 140 | }; 141 | 142 | if (!block.src) { 143 | return ; 144 | } 145 | 146 | return ( 147 | 155 | ); 156 | })} 157 | 158 | ); 159 | } 160 | 161 | render() 162 | { 163 | const { props, state } = this; 164 | 165 | if (state.articles.length) { 166 | return ( 167 | { 172 | switch (type) 173 | { 174 | case 'more': 175 | let articles = Object.assign([], state.articles); 176 | let nextArticles = Object.assign([], getImages(IMAGES, 30)); 177 | let last = articles[articles.length-1]; 178 | 179 | for (let i=0; i 199 | ); 200 | } else { 201 | return ( 202 | 203 | 204 | Loading.. 205 | 206 | ); 207 | } 208 | } 209 | 210 | } 211 | 212 | 213 | function getImages(getImages, count=5) 214 | { 215 | let images = new Array(count); 216 | 217 | for (let i=0; i , 69 | renderNotFound: () => , 70 | 71 | style: null, 72 | styleList: null, 73 | styleRow: null, 74 | styleBlock: null, 75 | styleHeader: null, 76 | styleFooter: null, 77 | }; 78 | 79 | constructor(props) { 80 | super(props); 81 | 82 | this.state = { 83 | blank: false, 84 | }; 85 | this.list = null; 86 | this.itemSize = 0; 87 | this.windowSize = { width: 0, height: 0 }; 88 | this.binds = { 89 | onEndReached: this.onEndReached.bind(this), 90 | renderRow: this.renderRow.bind(this), 91 | renderHeader: this.renderHeader.bind(this), 92 | renderFooter: this.renderFooter.bind(this), 93 | getItemLayout: this.getItemLayout.bind(this), 94 | }; 95 | this.innerMargin = [0,0]; 96 | this.outerMargin = [0,0]; 97 | } 98 | componentWillMount() { 99 | this.updateSize(this.props); 100 | } 101 | componentWillUpdate(nextProps) { 102 | const { props } = this; 103 | 104 | // checking for updateSize 105 | if ( 106 | nextProps.column !== props.column || 107 | nextProps.innerMargin !== props.innerMargin || 108 | nextProps.outerMargin !== props.outerMargin || 109 | (nextProps.width !== 'auto' && nextProps.width !== props.width) || 110 | (nextProps.width === 'auto' && this.windowSize !== Dimensions.get('window')) 111 | ) { 112 | this.updateSize(nextProps); 113 | } 114 | } 115 | shouldComponentUpdate(nextProps, nextState) { 116 | const { props, state } = this; 117 | 118 | if (state.blank !== nextState.blank) return true; 119 | if (props.items !== nextProps.items) return true; 120 | if (props.type !== nextProps.type) return true; 121 | 122 | return false; 123 | } 124 | 125 | 126 | /** 127 | * FUNCTIONS AREA 128 | */ 129 | 130 | /** 131 | * get inner margin 132 | * 133 | * @return {Number} 134 | */ 135 | getInnerMargin() { 136 | return (this.props.column > 1) ? this.innerMargin[0] : 0; 137 | } 138 | 139 | /** 140 | * get item size 141 | * 142 | * @return {Number} 143 | */ 144 | getItemSize(props) { 145 | let width = props.width === 'auto' ? this.windowSize.width : props.width; 146 | let innerMargin = (props.column - 1) * this.getInnerMargin(); 147 | 148 | return props.column > 1 ? (width - (innerMargin + (this.outerMargin[0] * 2))) / props.column : 'auto'; 149 | } 150 | 151 | /** 152 | * update viewport and block size 153 | * 154 | * @param {Object} props 155 | */ 156 | updateSize(props) { 157 | this.windowSize = Dimensions.get('window'); 158 | this.innerMargin = (typeof props.innerMargin === 'number') ? [props.innerMargin, props.innerMargin] : props.innerMargin; 159 | this.outerMargin = (typeof props.outerMargin === 'number') ? [props.outerMargin, props.outerMargin] : props.outerMargin; 160 | this.itemSize = this.getItemSize(props); 161 | } 162 | 163 | /** 164 | * on end reached 165 | */ 166 | onEndReached() { 167 | const { props } = this; 168 | 169 | if (props.useScrollEvent && props.type === 'ready') { 170 | props.load('more'); 171 | } 172 | } 173 | 174 | /** 175 | * get item layout 176 | * 177 | * @param {Array} data 178 | * @param {Number} index 179 | * @return {Object} 180 | */ 181 | getItemLayout(data, index) { 182 | const { props } = this; 183 | 184 | if (props.getItemLayout) { 185 | return props.getItemLayout(data, index); 186 | } else { 187 | if (props.itemHeight) { 188 | return { 189 | length: props.itemHeight, 190 | offset: ((props.itemHeight + this.innerMargin[1]) * index) + (this.outerMargin[1]), 191 | index 192 | }; 193 | } 194 | } 195 | } 196 | 197 | 198 | /** 199 | * RENDER AREA 200 | */ 201 | renderRow(o) { 202 | const { props } = this; 203 | 204 | return ( 205 | 215 | {props.renderRow({ 216 | item: o.item, 217 | index: o.index, 218 | size: this.itemSize === 'auto' ? this.windowSize.width : this.itemSize 219 | })} 220 | 221 | ); 222 | } 223 | renderHeader() { 224 | const { props } = this; 225 | 226 | return ( 227 | 232 | {!!props.renderHeader && props.renderHeader()} 233 | 234 | ); 235 | } 236 | renderFooter() { 237 | const { props } = this; 238 | 239 | return ( 240 | 245 | {!!props.renderFooter && props.renderFooter()} 246 | {props.type === 'loading' && ( 247 | 248 | )} 249 | 250 | ); 251 | } 252 | render() { 253 | const { props, state } = this; 254 | 255 | // check type `error` 256 | if (props.type === 'error') { 257 | return props.renderError(); 258 | } 259 | 260 | // check item count 261 | if (!(props.items && props.items.length)) { 262 | return props.renderNotFound(); 263 | } 264 | 265 | return ( 266 | 271 | {state.blank ? null : ( 272 | { this.list = r; }} 274 | data={props.items} 275 | keyExtractor={props.keyExtractor ? props.keyExtractor : (item, index) => `item_${index}`} 276 | initialNumToRender={props.pageSize} 277 | getItemLayout={(props.getItemLayout || props.itemHeight) ? this.binds.getItemLayout : null} 278 | renderItem={this.binds.renderRow} 279 | ListHeaderComponent={this.binds.renderHeader} 280 | ListFooterComponent={this.binds.renderFooter} 281 | numColumns={props.column} 282 | columnWrapperStyle={props.column > 1 && [ 283 | { marginLeft: 0 - this.getInnerMargin() + this.outerMargin[0] }, 284 | props.styleRow 285 | ]} 286 | refreshing={props.useRefresh && props.type === 'refresh'} 287 | onRefresh={props.useRefresh ? function() { props.load('refresh') } : null} 288 | onEndReachedThreshold={props.endReachedPosition} 289 | removeClippedSubviews={props.removeClippedSubviews} 290 | onEndReached={this.binds.onEndReached} 291 | debug={props.useDebug} 292 | style={[ css.list, props.styleList ]}/> 293 | )} 294 | 295 | ); 296 | } 297 | 298 | 299 | /** 300 | * METHOD AREA 301 | */ 302 | 303 | /** 304 | * re render 305 | */ 306 | reRender() { 307 | this.updateSize(this.props); 308 | this.setState({ blank: true }, () => { 309 | this.setState({ blank: false }); 310 | }); 311 | } 312 | 313 | } --------------------------------------------------------------------------------