├── 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 | }
--------------------------------------------------------------------------------