├── .gitignore
├── .npmignore
├── AndroidFloatSectionHeader.js
├── AndroidSwipeRefreshLayout.js
├── LICENSE
├── ListItem-android.js
├── ListItem-ios.js
├── ListItem.js
├── PullToRefreshListView-android.js
├── PullToRefreshListView-ios.js
├── PullToRefreshListView.js
├── README.md
├── RefreshView.js
├── android
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── reactnativecomponent
│ │ └── swiperefreshlayout
│ │ ├── OnEvChangeListener.java
│ │ ├── RCTLazyLoadView.java
│ │ ├── RCTLazyLoadViewManager.java
│ │ ├── RCTSwipeRefreshLayout.java
│ │ ├── RCTSwipeRefreshLayoutManager.java
│ │ ├── RCTSwipeRefreshLayoutPackage.java
│ │ ├── TouchEvent.java
│ │ ├── TouchUpEvent.java
│ │ └── WindowVisibilityChangeEvent.java
│ └── res
│ └── values
│ └── strings.xml
├── constants.js
├── easing.js
├── note.md
├── package.json
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.[aod]
2 | *.DS_Store
3 | .DS_Store
4 | *Thumbs.db
5 | *.iml
6 | .gradle
7 | .idea
8 | node_modules
9 | npm-debug.log
10 | /android/build
11 | /ios/**/*xcuserdata*
12 | /ios/**/*xcshareddata*
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.[aod]
2 | *.DS_Store
3 | .DS_Store
4 | *Thumbs.db
5 | *.iml
6 | .gradle
7 | .idea
8 | node_modules
9 | npm-debug.log
10 | /android/build
11 | /ios/**/*xcuserdata*
12 | /ios/**/*xcshareddata*
--------------------------------------------------------------------------------
/AndroidFloatSectionHeader.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | } from 'react'
4 | import PropTypes from 'prop-types';
5 | import {
6 | View,
7 | ViewPropTypes,
8 | } from 'react-native'
9 |
10 | export default class AndroidFloatSectionHeader extends Component {
11 |
12 | static propTypes = {
13 | ...ViewPropTypes,
14 | floatSectionHeaderWidth: PropTypes.number.isRequired,
15 | renderChildren: PropTypes.func.isRequired,
16 | }
17 |
18 | constructor (props) {
19 | super(props)
20 | this.state = {
21 | sectionID: '',
22 | hidden: true,
23 | }
24 | }
25 |
26 | render () {
27 | return (
28 |
32 | {this.props.renderChildren(this.state.sectionID)}
33 |
34 | )
35 | }
36 |
37 | setSectionID = (sectionID) => {
38 | this.setState({
39 | sectionID,
40 | })
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/AndroidSwipeRefreshLayout.js:
--------------------------------------------------------------------------------
1 |
2 | import React, {
3 | Component,
4 | } from 'react'
5 | import PropTypes from 'prop-types';
6 | import {
7 | View,
8 | requireNativeComponent,
9 | Platform,
10 | ViewPropTypes,
11 | } from 'react-native'
12 |
13 | export default class AndroidSwipeRefreshLayout extends Component {
14 |
15 | static propTypes = {
16 | ...ViewPropTypes,
17 | refreshing: PropTypes.bool,
18 | enabledPullUp: PropTypes.bool,
19 | enabledPullDown: PropTypes.bool,
20 | onSwipe: PropTypes.func,
21 | onRefresh: PropTypes.func,
22 | }
23 |
24 | setNativeProps(props) {
25 | this._nativeSwipeRefreshLayout.setNativeProps(props)
26 | }
27 |
28 | render() {
29 |
30 | return (
31 | this._nativeSwipeRefreshLayout = component }
34 | onSwipe={this._onSwipe}
35 | onSwipeRefresh={this._onRefresh}
36 | />
37 | );
38 | }
39 |
40 | _onSwipe = (e) => {
41 | this.props.onSwipe(e.nativeEvent.movement)
42 | }
43 |
44 | _onRefresh = () => {
45 | this.props.onRefresh()
46 | }
47 |
48 | }
49 |
50 | const NativeSwipeRefreshLayout = Platform.OS == 'ios' ? View : requireNativeComponent('RCTSwipeRefreshLayout', AndroidSwipeRefreshLayout)
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016
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 |
--------------------------------------------------------------------------------
/ListItem-android.js:
--------------------------------------------------------------------------------
1 |
2 | import React, {
3 | Component,
4 | } from 'react'
5 | import PropTypes from 'prop-types';
6 | import {
7 | View,
8 | requireNativeComponent,
9 | Platform,
10 | ViewPropTypes,
11 | } from 'react-native'
12 |
13 | export default class ListItem extends Component {
14 |
15 |
16 | static propTypes = {
17 | ...ViewPropTypes,
18 | renderChildren: PropTypes.func,
19 | }
20 |
21 | constructor(props) {
22 | super(props)
23 | this.state = {
24 | hidden: true,
25 | }
26 | }
27 |
28 | render() {
29 | //console.log(`rowID = ${this.props.rowID} -> hidden = ${this.state.hidden}`)
30 | //return (
31 | //
32 | // {!this.state.hidden ? this.props.children : null}
33 | //
34 | //)
35 | return (
36 | this._nativeComponent = component }
38 | {...this.props}
39 | onWindowVisibilityChange={this._onWindowVisibilityChange}>
40 | {this.props.renderChildren ?
41 | this.props.renderChildren.call(this, this.state.hidden)
42 | : !this.state.hidden ? this.props.children : null}
43 |
44 | )
45 | }
46 |
47 | _onWindowVisibilityChange = (e) => {
48 | let hidden = e.nativeEvent.hidden
49 | //console.log(`hidden = ${hidden}`)
50 | this.setState({
51 | hidden,
52 | })
53 | }
54 | }
55 |
56 | const NativeListItem = Platform.OS == 'ios' ? View : requireNativeComponent('RCTLazyLoadView', ListItem)
57 |
--------------------------------------------------------------------------------
/ListItem-ios.js:
--------------------------------------------------------------------------------
1 |
2 | import React, {
3 | Component,
4 | } from 'react'
5 | import PropTypes from 'prop-types';
6 | import {
7 | View,
8 | ViewPropTypes,
9 | } from 'react-native'
10 |
11 | export default class ListItem extends Component {
12 |
13 |
14 | static propTypes = {
15 | ...ViewPropTypes,
16 | }
17 |
18 | constructor(props) {
19 | super(props)
20 | this.state = {
21 | hidden: true,
22 | }
23 | }
24 |
25 | render() {
26 | //console.log(`this.state.hidden = ${this.state.hidden}`)
27 | return (
28 |
29 | {!this.state.hidden ? this.props.children : null}
30 |
31 | )
32 | }
33 |
34 | show() {
35 | this.setState({
36 | hidden: false,
37 | })
38 | }
39 |
40 | hide() {
41 | this.setState({
42 | hidden: true,
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ListItem.js:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | Platform,
4 | } from 'react-native'
5 |
6 | import AndroidListItem from './ListItem-android'
7 | import IOSListItem from './ListItem-ios'
8 |
9 | let ListItem
10 |
11 | if(Platform.OS == 'ios') {
12 | ListItem = IOSListItem
13 | }
14 | else {
15 | ListItem = AndroidListItem
16 | }
17 |
18 | export default ListItem
--------------------------------------------------------------------------------
/PullToRefreshListView-android.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A smart pull-down-refresh and pull-up-loadmore react-native listview
3 | * https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview/
4 | * Released under the MIT license
5 | * Copyright (c) 2016 react-native-component
6 | */
7 |
8 | import React, {
9 | Component,
10 | } from 'react'
11 | import PropTypes from 'prop-types';
12 | import {
13 | View,
14 | ScrollView,
15 | ListView,
16 | StyleSheet,
17 | Text,
18 | Platform,
19 | Dimensions,
20 | } from 'react-native'
21 |
22 | //import TimerEnhance from '../react-native-smart-timer-enhance'
23 | import TimerEnhance from 'react-native-smart-timer-enhance'
24 | import { withinErrorMargin, } from './utils'
25 | import constants, {
26 | viewType,
27 | viewState,
28 | refreshViewType,
29 | refreshAnimationDuration,
30 | scrollBounceAnimationDuration,
31 | } from './constants'
32 | import { easeOutCirc, } from './easing'
33 | import RefreshView from './RefreshView'
34 | import AndroidSwipeRefreshLayout from './AndroidSwipeRefreshLayout'
35 | import ListItem from './ListItem'
36 | import FloatSectionHeader from './AndroidFloatSectionHeader'
37 |
38 | //temp log code
39 | //import Temp from 'react-native-fs'
40 |
41 | const styles = StyleSheet.create({
42 | header: {
43 | justifyContent: 'flex-end',
44 | },
45 | footer: {
46 | justifyContent: 'flex-start',
47 | },
48 | shrink: {
49 | height: 0,
50 | },
51 | marginVertical: {
52 | marginTop: 0,
53 | marginBottom: 0,
54 | marginVertical: 0,
55 | },
56 | paddingVertical: {
57 | paddingTop: 0,
58 | paddingBottom: 0,
59 | paddingVertical: 0,
60 | }
61 | })
62 |
63 | const {width: deviceWidth, } = Dimensions.get('window')
64 | const keySeparator = ',';
65 |
66 | class PullToRefreshListView extends Component {
67 |
68 | static constants = constants
69 |
70 | static defaultProps = {
71 | viewType: viewType.scrollView,
72 | pullUpDistance: 50,
73 | pullUpStayDistance: 35,
74 | pullDownDistance: 50,
75 | pullDownStayDistance: 35,
76 | enabledPullUp: true,
77 | enabledPullDown: true,
78 | autoLoadMore: false,
79 | scrollEventThrottle: 16,
80 | dataSource: new ListView.DataSource({
81 | rowHasChanged: (r1, r2) => r1 !== r2,
82 | }),
83 | renderRow: () => null,
84 | renderScrollComponent: props => ,
85 | onEndReachedThreshold: StyleSheet.hairlineWidth, //0,
86 | initialListSize: 10,
87 | stickyHeaderIndices: [],
88 | pageSize: 1,
89 | scrollRenderAheadDistance: 1000,
90 | floatSectionHeaderWidth: deviceWidth,
91 | pageTop: 0,
92 | }
93 |
94 | static propTypes = {
95 | ...ListView.propTypes,
96 | pageTop: PropTypes.number,
97 | renderFloatSectionHeader: PropTypes.func,
98 | floatSectionHeaderWidth: PropTypes.number,
99 | listSectionProps: PropTypes.shape(View.propTypes),
100 | listItemProps: PropTypes.shape(View.propTypes),
101 | renderRowWithVisibility: PropTypes.bool,
102 | viewType: PropTypes.oneOf([
103 | viewType.scrollView,
104 | viewType.listView,
105 | ]),
106 | pullUpDistance: PropTypes.number,
107 | pullUpStayDistance: PropTypes.number,
108 | pullDownDistance: PropTypes.number,
109 | pullDownStayDistance: PropTypes.number,
110 | enabledPullUp: PropTypes.bool,
111 | enabledPullDown: PropTypes.bool,
112 | autoLoadMore: PropTypes.bool,
113 | onRefresh: PropTypes.func,
114 | onLoadMore: PropTypes.func,
115 | }
116 |
117 | constructor (props) {
118 | super(props)
119 | this.state = {}
120 | let {refresh_none, load_more_none} = viewState
121 |
122 | if (props.autoLoadMore && props.viewType == viewType.listView) {
123 | this._onEndReached = () => {
124 | let { refreshing, load_more_none, loading_more,} = viewState
125 | //if (this._refreshState != refreshing && this._loadMoreState == load_more_none) {
126 | if (this._canLoadMore && this._refreshState != refreshing && this._loadMoreState == load_more_none) {
127 | this._loadMoreState = loading_more
128 | this._footer.setState({
129 | pullState: this._loadMoreState,
130 | })
131 |
132 | props.onLoadMore && props.onLoadMore()
133 | }
134 | }
135 | }
136 |
137 | this._refreshState = refresh_none
138 | this._loadMoreState = load_more_none
139 | this._refreshBackAnimating = false
140 | this._loadMoreBackAnimating = false
141 | this._afterRefreshBacked = false
142 | this._afterLoadMoreBacked = false
143 | this._beginTimeStamp = null
144 | this._beginResetScrollTopTimeStamp = null
145 | this._refreshBackAnimationFrame = null
146 | this._touching = false
147 | this._scrollY = 0
148 | this._lastScrollY = 0
149 | this._fixedScrollY = 0
150 | this._refreshFixScrollY = 0
151 | this._paddingBlankDistance = 0
152 |
153 | this._listSectionRefs = {}
154 | this._listItemRefs = {}
155 |
156 | this._headerHeight = 0
157 | this._canLoadMore = false
158 | this._autoLoadFooterHeight = 0
159 | }
160 |
161 | render () {
162 | return (
163 | this._swipeRefreshLayout = component }
165 | style={{flex: 1,}}
166 | enabledPullUp={this.props.enabledPullUp}
167 | enabledPullDown={this.props.enabledPullDown}
168 | onSwipe={this._onSwipe}
169 | onRefresh={this._onRefresh}>
170 | { this.props.viewType == viewType.scrollView ?
171 | this._scrollView = component }
173 | {...this.props}
174 | style={[this.props.style, styles.paddingVertical,]}
175 | contentContainerStyle={[this.props.contentContainerStyle, styles.marginVertical,]}
176 | onLayout={this._onLayout}
177 | onContentSizeChange={this._onContentSizeChange}
178 | onResponderGrant={this._onResponderGrant}
179 | onScroll={this._onScroll}
180 | onResponderRelease={this._onResponderRelease}>
181 | {this._renderHeader()}
182 | {this.props.children}
183 | {this._renderFooter()}
184 | :
185 | this._scrollView = component }
187 | {...this.props}
188 | style={[this.props.style, styles.paddingVertical,]}
189 | contentContainerStyle={[this.props.contentContainerStyle, styles.marginVertical,]}
190 | onEndReached={this._onEndReached}
191 | onLayout={this._onLayout}
192 | onContentSizeChange={this._onContentSizeChange}
193 | onResponderGrant={this._onResponderGrant}
194 | onScroll={this._onScroll}
195 | onResponderRelease={this._onResponderRelease}
196 | renderSectionHeader={this._renderSectionHeader}
197 | renderRow={this._renderRow}
198 | renderHeader={this._renderHeader}
199 | renderFooter={this._renderFooter}
200 | renderScrollComponent={ props => this._innerScrollView = component } {...props} /> }/> }
201 | { this.props.renderFloatSectionHeader && this.props.listSectionProps ?
202 | this._floatSectionHeader = component }
204 | {...this.props.listSectionProps}
205 | floatSectionHeaderWidth={this.props.floatSectionHeaderWidth}
206 | renderChildren={this.props.renderFloatSectionHeader}/> : null }
207 |
208 | )
209 | }
210 |
211 | componentDidMount () {
212 | let {renderSectionHeader, listSectionProps, dataSource} = this.props
213 | //android float section header
214 | if (renderSectionHeader && listSectionProps) {
215 | //dataSource.sectionIdentities.length will be equal to 0 using 'beginRefresh'
216 | if (dataSource.sectionIdentities.length == 0) {
217 | return
218 | }
219 | let firstSectionID = dataSource.sectionIdentities[ 0 ]
220 | //set float section header
221 | this._floatSectionHeader.setSectionID(firstSectionID)
222 | this._floatSectionHeader.setState({
223 | hidden: false,
224 | })
225 | }
226 | }
227 |
228 | setNativeProps = (props) => {
229 | this._scrollView.setNativeProps(props)
230 | }
231 |
232 | beginRefresh = (bounceDisabled) => {
233 | this._swipeRefreshLayout.setNativeProps({
234 | refreshing: true,
235 | })
236 | this._scrollView.setNativeProps({
237 | scrollEnabled: false,
238 | })
239 | //this.requestAnimationFrame(this._resetReverseHeaderLayout)
240 | if (!bounceDisabled) {
241 | this.requestAnimationFrame(this._resetReverseHeaderLayout)
242 | }
243 | else {
244 | //console.log(`beginRefresh onRefresh()`)
245 | this.props.onRefresh && this.props.onRefresh()
246 | }
247 | let {refreshing,} = viewState
248 | this._refreshState = refreshing
249 | this._header.setState({
250 | pullState: this._refreshState,
251 | pullDistancePercent: 1,
252 | })
253 |
254 | ////force hide footer
255 | //this._footer.setNativeProps({
256 | // style: {
257 | // opacity: 0,
258 | // }
259 | //})
260 |
261 | //this.props.onRefresh && this.props.onRefresh() //move to _resetReverseHeaderLayout and _resetRefreshScrollTop
262 | //this._listSectionRefs = {}
263 | //this._listItemRefs = {}
264 | }
265 |
266 | endRefresh = (bounceDisabled) => {
267 | this._scrollView.setNativeProps({
268 | scrollEnabled: false
269 | })
270 |
271 | let {refresh_none, loaded_all, load_more_none} = viewState
272 | let {pullDownStayDistance, dataSource, renderSectionHeader, listSectionProps,} = this.props
273 | this._refreshState = refresh_none
274 | this._header.setState({
275 | pullState: this._refreshState,
276 | })
277 |
278 | this._refreshBackAnimating = true
279 |
280 | //if (this._scrollY < pullDownStayDistance) {
281 | if (!bounceDisabled && this._scrollY < pullDownStayDistance) {
282 | this.requestAnimationFrame(this._resetHeaderLayout)
283 | }
284 | else {
285 | //this._swipeRefreshLayout.setNativeProps({
286 | // refreshing: false,
287 | //})
288 | //this._scrollView.setNativeProps({
289 | // scrollEnabled: true,
290 | //})
291 | this._header.setNativeProps({
292 | style: {
293 | height: 0,
294 | }
295 | })
296 | this._headerHeight = 0
297 |
298 | //this._scrollView.scrollTo({ y: this._scrollY - pullDownStayDistance, animated: false, })
299 | if (!bounceDisabled) {
300 | this._scrollView.scrollTo({ y: this._scrollY - pullDownStayDistance, animated: false, })
301 | }
302 | this._beginTimeStamp = null
303 | this._refreshBackAnimating = false
304 | this._afterRefreshBacked = true
305 |
306 | this._afterDirectRefresh = true
307 |
308 | //this._setPaddingBlank()
309 | this._setPaddingBlank(bounceDisabled)
310 |
311 | ////force show footer
312 | //this._footer.setNativeProps({
313 | // style: {
314 | // opacity: 1,
315 | // }
316 | //})
317 |
318 | //reset loadMoreState to load_more_none
319 | if (this._loadMoreState == loaded_all) {
320 | this._loadMoreState = load_more_none
321 | this._footer.setState({
322 | pullState: this._loadMoreState,
323 | pullDistancePercent: 0,
324 | })
325 | }
326 |
327 | this._swipeRefreshLayout.setNativeProps({
328 | refreshing: false,
329 | })
330 | this._scrollView.setNativeProps({
331 | scrollEnabled: true,
332 | })
333 |
334 | if (renderSectionHeader && listSectionProps) {
335 | this._floatSectionHeader.setState({
336 | hidden: false,
337 | })
338 | let firstSectionID = dataSource.sectionIdentities[ 0 ]
339 | //reset float section header
340 | this._floatSectionHeader.setSectionID(firstSectionID)
341 | }
342 |
343 | }
344 | }
345 |
346 | endLoadMore = (loadedAll) => {
347 | this._scrollView.setNativeProps({
348 | scrollEnabled: false
349 | })
350 |
351 | let {load_more_none, loaded_all} = viewState
352 | let {autoLoadMore} = this.props
353 | if (!loadedAll) {
354 | this._loadMoreState = load_more_none
355 | }
356 | else {
357 | this._loadMoreState = loaded_all
358 | }
359 | this._footer.setState({
360 | pullState: this._loadMoreState,
361 | })
362 |
363 | if (!autoLoadMore) {
364 | this._loadMoreBackAnimating = true
365 |
366 | if (this._scrollY >= this._scrollViewContentHeight - this._scrollViewContainerHeight) {
367 | this.requestAnimationFrame(this._resetFooterLayout)
368 | }
369 | else {
370 | //this._swipeRefreshLayout.setNativeProps({
371 | // refreshing: false,
372 | //})
373 | //this._scrollView.setNativeProps({
374 | // scrollEnabled: true,
375 | //})
376 | this._footer.setNativeProps({
377 | style: {
378 | height: 0,
379 | }
380 | })
381 | this._scrollView.scrollTo({ y: this._scrollY, animated: false, })
382 |
383 | this._beginTimeStamp = null
384 | this._loadMoreBackAnimating = false
385 | this._afterLoadMoreBacked = true
386 |
387 | this._setPaddingBlank()
388 |
389 | this._swipeRefreshLayout.setNativeProps({
390 | refreshing: false,
391 | })
392 | this._scrollView.setNativeProps({
393 | scrollEnabled: true,
394 | })
395 | }
396 | }
397 | else {
398 | this._setPaddingBlank()
399 |
400 | this._swipeRefreshLayout.setNativeProps({
401 | refreshing: false,
402 | })
403 | this._scrollView.setNativeProps({
404 | scrollEnabled: true,
405 | })
406 | }
407 | }
408 |
409 | _onSwipe = (movement) => {
410 |
411 | this._touching = true
412 |
413 | if(this._touchingScrollY == null) {
414 | this._touchingScrollY = this._scrollY
415 | }
416 |
417 | if (this._refreshBackAnimationFrame) {
418 | this._beginResetScrollTopTimeStamp = null
419 | this._moveMent = 0
420 | this.cancelAnimationFrame(this._refreshBackAnimationFrame)
421 | }
422 |
423 | let {refresh_none, refresh_idle, will_refresh, refreshing,
424 | load_more_none, load_more_idle, will_load_more, loading_more, loaded_all,} = viewState
425 | let {pullUpDistance, pullDownDistance, autoLoadMore, enabledPullUp, enabledPullDown, renderSectionHeader, listSectionProps,} = this.props
426 |
427 | if (movement > 0) {
428 | if (enabledPullDown) {
429 | if (renderSectionHeader && listSectionProps) {
430 | this._floatSectionHeader.setState({
431 | hidden: true,
432 | })
433 | }
434 |
435 | if (this._refreshState == refresh_none) {
436 | this._refreshState = refresh_idle
437 | }
438 | this._header.setNativeProps({
439 | style: {
440 | height: movement,
441 | },
442 | })
443 | this._headerHeight = movement
444 |
445 | this._moveMent = movement
446 | this._lastMoveMent = movement
447 |
448 | if (this._refreshState == refresh_idle || this._refreshState == will_refresh) {
449 | if (movement >= pullDownDistance) {
450 | if (this._refreshState == refresh_idle) {
451 | this._refreshState = will_refresh
452 | }
453 | }
454 | else {
455 | if (this._refreshState == will_refresh) {
456 | this._refreshState = refresh_idle
457 | }
458 | }
459 | this._header.setState({
460 | pullState: this._refreshState,
461 | pullDistancePercent: movement / pullDownDistance,
462 | })
463 | }
464 | }
465 | }
466 | else if (movement < 0) {
467 | //if (enabledPullUp && !autoLoadMore) {
468 | if (this._canLoadMore && enabledPullUp && !autoLoadMore) {
469 | if (this._loadMoreState == load_more_none) {
470 | this._loadMoreState = load_more_idle
471 | this._footer.setState({
472 | pullState: this._loadMoreState,
473 | pullDistancePercent: Math.abs(movement) / pullUpDistance,
474 | })
475 | }
476 |
477 | this._footer.setNativeProps({
478 | style: {
479 | height: Math.abs(movement),
480 | },
481 | })
482 | this._scrollView.scrollTo({ y: this._scrollY + Math.abs(movement) / 2, animated: false, })
483 |
484 | this._moveMent = movement
485 | this._lastMoveMent = movement
486 |
487 | if (this._loadMoreState == load_more_idle || this._loadMoreState == will_load_more) {
488 | if (Math.abs(movement) > pullUpDistance) {
489 | if (this._loadMoreState == load_more_idle) {
490 | this._loadMoreState = will_load_more
491 | }
492 | }
493 | else {
494 | if (this._loadMoreState == will_load_more) {
495 | this._loadMoreState = load_more_idle
496 | }
497 | }
498 | this._footer.setState({
499 | pullState: this._loadMoreState,
500 | pullDistancePercent: Math.abs(movement) / pullUpDistance,
501 | })
502 | }
503 | }
504 | }
505 | else {
506 | if (this._lastMoveMent > 0) {
507 | this._header.setNativeProps({
508 | style: {
509 | height: 0,
510 | },
511 | })
512 | this._headerHeight = 0
513 | }
514 | else {
515 | this._footer.setNativeProps({
516 | style: {
517 | height: 0,
518 | },
519 | })
520 | //this._scrollView.scrollTo({ y: this._scrollY, animated: false, })
521 | this._scrollView.scrollTo({ y: this._touchingScrollY, animated: false, })
522 | }
523 | }
524 | }
525 |
526 | _onRefresh = () => {
527 | this._touchingScrollY = null
528 |
529 | this._touching = false
530 |
531 | if (this._moveMent > 0) {
532 | let {will_refresh, refreshing, } = viewState
533 |
534 | if (this._refreshState == will_refresh) {
535 |
536 | //disable swipe event
537 | this._swipeRefreshLayout.setNativeProps({
538 | refreshing: true,
539 | })
540 |
541 | this._refreshState = refreshing
542 | this._header.setState({
543 | pullState: this._refreshState,
544 | pullDistancePercent: 0,
545 | })
546 |
547 | //this.props.onRefresh && this.props.onRefresh() //move to _resetReverseHeaderLayout and _resetRefreshScrollTop
548 | //this._listSectionRefs = {}
549 | //this._listItemRefs = {}
550 | }
551 |
552 | this._refreshBackAnimationFrame = this.requestAnimationFrame(this._resetRefreshScrollTop)
553 | }
554 | else {
555 | if (this._moveMent < 0) {
556 | let {will_load_more, loading_more, } = viewState
557 | if (this._loadMoreState == will_load_more) {
558 |
559 | //disable swipe event
560 | this._swipeRefreshLayout.setNativeProps({
561 | refreshing: true,
562 | })
563 |
564 | this._loadMoreState = loading_more
565 | this._footer.setState({
566 | pullState: this._loadMoreState,
567 | pullDistancePercent: 0,
568 | })
569 |
570 | //this.props.onLoadMore && this.props.onLoadMore() //move to _resetLoadMoreScrollTop
571 | }
572 |
573 | this._refreshBackAnimationFrame = this.requestAnimationFrame(this._resetLoadMoreScrollTop)
574 | }
575 | }
576 | }
577 |
578 | _setPaddingBlank = (paddingDisabled) => {
579 | let innerViewRef = this._scrollView.refs.InnerScrollView || this._scrollView._innerViewRef || this._innerScrollView.refs.InnerScrollView || this._innerScrollView._innerViewRef
580 | innerViewRef.measure((ox, oy, width, height, px, py) => {
581 |
582 | let opacity
583 | let footerHeight = this.props.autoLoadMore ? this._autoLoadFooterHeight : 0
584 | //console.log(`footerHeight = ${footerHeight}, height = ${height}, this._paddingBlankDistance = ${this._paddingBlankDistance}, this._headerHeight = ${this._headerHeight}, this._scrollViewContainerHeight = ${this._scrollViewContainerHeight},`)
585 | if(height - this._paddingBlankDistance - this._headerHeight - footerHeight < this._scrollViewContainerHeight) {
586 | opacity = 0
587 | this._canLoadMore = false
588 | }
589 | else {
590 | //console.log(`this._afterDirectRefresh = ${this._afterDirectRefresh}`)
591 | if(!this._afterDirectRefresh) {
592 | opacity = 1
593 | this._canLoadMore = true
594 | }
595 | else {
596 | opacity = 0
597 | this._canLoadMore = false
598 | }
599 |
600 | }
601 | //console.log(`this._canLoadMore = ${this._canLoadMore}`)
602 |
603 | if (!paddingDisabled && height - this._paddingBlankDistance < this._scrollViewContainerHeight) {
604 | this._paddingBlankDistance = this._scrollViewContainerHeight - (height - this._paddingBlankDistance)
605 | }
606 | else {
607 | this._paddingBlankDistance = 0
608 | }
609 |
610 | if(!this._footer) {
611 | return
612 | }
613 |
614 | //console.log(`opacity = ${opacity}, this._canLoadMore = ${this._canLoadMore}, this._paddingBlankDistance = ${this._paddingBlankDistance}, this._afterDirectRefresh = ${this._afterDirectRefresh}`)
615 | this._footer.setNativeProps({
616 | style: {
617 | opacity,
618 | marginTop: this._paddingBlankDistance,
619 | }
620 | })
621 | })
622 | }
623 |
624 | _onLayout = (e) => {
625 | if (this._scrollViewContainerHeight == null) {
626 | this._scrollViewContainerHeight = e.nativeEvent.layout.height
627 | }
628 |
629 | this._setPaddingBlank()
630 |
631 | this.props.onLayout && this.props.onLayout(e)
632 | }
633 |
634 | //ensure that onContentSizeChange must be triggered while ending resetHeaderLayout/resetFooterLayout animation
635 | _onContentSizeChange = (contentWidth, contentHeight) => {
636 | let {refreshing, loading_more} = viewState
637 | if (this._scrollViewContentHeight == null
638 | || ((this._refreshState != refreshing && !this._refreshBackAnimating)
639 | //&& (this._loadMoreState != loading_more && !this._loadMoreBackAnimating))) {
640 | && ( this.props.autoLoadMore || (!this.props.autoLoadMore && this._loadMoreState != loading_more && !this._loadMoreBackAnimating) ))) {
641 | this._scrollViewContentHeight = contentHeight
642 |
643 | if (this._afterDirectRefresh) {
644 | this._afterDirectRefresh = false
645 |
646 | let {pullDownStayDistance} = this.props
647 |
648 | if (this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight + pullDownStayDistance) {
649 | this._scrollView.scrollTo({
650 | y: this._scrollViewContentHeight - this._scrollViewContainerHeight,
651 | animated: false,
652 | })
653 | }
654 |
655 | //this._setPaddingBlank()
656 | }
657 |
658 | }
659 |
660 | this.props.onContentSizeChange && this.props.onContentSizeChange(contentWidth, contentHeight)
661 | }
662 |
663 | _onScroll = (e) => {
664 | let {refreshing, load_more_none, loading_more, } = viewState
665 | let {autoLoadMore, renderSectionHeader, listSectionProps, pageTop, } = this.props
666 | this._scrollY = e.nativeEvent.contentOffset.y
667 |
668 | /**
669 | * (occurs on react-native 0.32, and maybe also occurs on react-native 0.30+)Android ScrollView scrolls to bottom may occur scrollTop larger than it should be
670 | * only occurs on android 4.4-
671 | */
672 | //if(this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight) {
673 | // this._scrollY = this._scrollViewContentHeight - this._scrollViewContainerHeight
674 | // this._scrollView.scrollTo({y: this._scrollY, animated: false, })
675 | //}
676 |
677 | //use onEndReached handler when viewType is 'listView' to fix double triggering onLoadMore sometimes, but no idea when viewType is 'scrollView'
678 | if (this.props.viewType == viewType.scrollView) {
679 | if (autoLoadMore && withinErrorMargin(this._scrollY, this._scrollViewContentHeight - this._scrollViewContainerHeight - this.props.onEndReachedThreshold)
680 | || this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight - this.props.onEndReachedThreshold) {
681 | if (this._refreshState != refreshing && this._loadMoreState == load_more_none) {
682 | //disable swipe event
683 | this._swipeRefreshLayout.setNativeProps({
684 | refreshing: true,
685 | })
686 |
687 | this._loadMoreState = loading_more
688 | this._footer.setState({
689 | pullState: this._loadMoreState,
690 | })
691 |
692 | this.props.onLoadMore && this.props.onLoadMore()
693 | }
694 | }
695 | }
696 | //viewType.listView
697 | else {
698 | //android float section header
699 | if (renderSectionHeader && listSectionProps) {
700 |
701 | if (this._refreshState == refreshing) {
702 | if (this._scrollY >= this._refreshingHeaderHeight) {
703 | if (this._floatSectionHeader.state.hidden && this._beginResetScrollTopTimeStamp == null) {
704 | this._floatSectionHeader.setState({
705 | hidden: false,
706 | })
707 | }
708 | }
709 | else {
710 | if (!this._floatSectionHeader.state.hidden) {
711 | this._floatSectionHeader.setState({
712 | hidden: true,
713 | })
714 | }
715 | }
716 | }
717 |
718 | let listItemKeys = Object.keys(this._listItemRefs)
719 | for (let i = 0, len = listItemKeys.length; i < len; i++) {
720 | let rowID = listItemKeys[ i ]
721 | let { component, sectionID, } = this._listItemRefs[ rowID ]
722 | //console.log(`rowID = ${rowID}, component.state.hidden = ${component.state.hidden}`)
723 | if (component && !component.state.hidden) {
724 | component._nativeComponent.measure((ox, oy, width, height, px, py) => {
725 | //console.log(`row -> py = ${py}, height = ${height}, pageTop = ${pageTop}, rowID = ${rowID}, sectionID = ${sectionID}`)
726 | if (py <= height + pageTop) {
727 | //set float section header
728 | this._floatSectionHeader.setSectionID(sectionID)
729 | }
730 | })
731 | return //do this only once
732 | }
733 | }
734 | //Object.keys(this._listItemRefs).every( (rowID) => {
735 | // let { component, sectionID, } = this._listItemRefs[rowID]
736 | // console.log(`rowID = ${rowID}, component.state.hidden = ${component.state.hidden}`)
737 | // //if(!component.state.hidden) {
738 | // // component._nativeComponent.measure( (ox, oy, width, height, px, py) => {
739 | // // //console.log(`row -> py = ${py}, height = ${height}, pageTop = ${pageTop}, rowID = ${rowID}, sectionID = ${sectionID}`)
740 | // // if (py <= height + pageTop) {
741 | // // this._floatSectionHeader.setSectionID(sectionID)
742 | // // }
743 | // // })
744 | // // return false //do this only once
745 | // //}
746 | //})
747 |
748 | let listSectionKeys = Object.keys(this._listSectionRefs)
749 | for (let i = 0, len = listSectionKeys.length; i < len; i++) {
750 | let sectionID = listSectionKeys[ i ]
751 | let component = this._listSectionRefs[ sectionID ]
752 | //console.log(`sectionID = ${sectionID}, component.state.hidden = ${component.state.hidden}`)
753 | if (component && !component.state.hidden) {
754 | component._nativeComponent.measure((ox, oy, width, height, px, py) => {
755 | //console.log(`section -> py = ${py}, height = ${height}, pageTop = ${pageTop}, sectionID = ${sectionID}`)
756 | if (py <= height + pageTop) {
757 | this._floatSectionHeader.setSectionID(sectionID)
758 | }
759 | })
760 | return //do this only once
761 | }
762 | }
763 |
764 | //Object.keys(this._listSectionRefs).every((sectionID) => {
765 | // let component = this._listSectionRefs[ sectionID ]
766 | // console.log(`sectionID = ${sectionID}, component.state.hidden = ${component.state.hidden}`)
767 | // //if(!component.state.hidden) {
768 | // // component._nativeComponent.measure( (ox, oy, width, height, px, py) => {
769 | // // //console.log(`section -> py = ${py}, height = ${height}, pageTop = ${pageTop}, sectionID = ${sectionID}`)
770 | // // if (py <= height + pageTop) {
771 | // // this._floatSectionHeader.setSectionID(sectionID)
772 | // // }
773 | // // })
774 | // // return false //do this only once
775 | // //}
776 | //})
777 | }
778 | }
779 |
780 | this.props.onScroll && this.props.onScroll(e)
781 | }
782 |
783 | _resetReverseHeaderLayout = (timestamp) => {
784 | let {pullDownStayDistance} = this.props
785 | let headerHeight
786 | if (!this._beginTimeStamp) {
787 | headerHeight = pullDownStayDistance
788 | this._beginTimeStamp = timestamp
789 | }
790 | else {
791 | headerHeight = pullDownStayDistance * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
792 | if (headerHeight > pullDownStayDistance) {
793 | headerHeight = pullDownStayDistance
794 | }
795 | }
796 | this._header.setNativeProps({
797 | style: {
798 | height: headerHeight,
799 | }
800 | })
801 | this._headerHeight = headerHeight
802 |
803 | if (timestamp - this._beginTimeStamp > refreshAnimationDuration) {
804 | this._header.setNativeProps({
805 | style: {
806 | height: pullDownStayDistance,
807 | }
808 | })
809 | this._headerHeight = pullDownStayDistance
810 |
811 | this._beginTimeStamp = null
812 | this._refreshBackAnimating = false
813 | this._afterRefreshBacked = true
814 |
815 | //console.log(`_resetReverseHeaderLayout onRefresh()`)
816 | this.props.onRefresh && this.props.onRefresh()
817 |
818 | return
819 | }
820 |
821 | return this.requestAnimationFrame(this._resetReverseHeaderLayout)
822 | }
823 |
824 | _resetRefreshScrollTop = (timestamp) => {
825 | let {pullDownStayDistance, renderSectionHeader, listSectionProps, } = this.props
826 | let {refreshing} = viewState
827 | if (this._refreshState != refreshing) {
828 | pullDownStayDistance = 0
829 | }
830 | let headerHeight
831 | if (!this._beginResetScrollTopTimeStamp) {
832 | headerHeight = this._moveMent
833 | this._beginResetScrollTopTimeStamp = timestamp
834 | }
835 | else {
836 | let percent = (timestamp - this._beginResetScrollTopTimeStamp) / scrollBounceAnimationDuration
837 | headerHeight = this._moveMent - (this._moveMent - pullDownStayDistance) * easeOutCirc(percent, scrollBounceAnimationDuration * percent, 0, 1, scrollBounceAnimationDuration)
838 | }
839 |
840 | if (headerHeight < pullDownStayDistance) {
841 | headerHeight = pullDownStayDistance
842 | }
843 |
844 | this._header.setNativeProps({
845 | style: {
846 | height: headerHeight,
847 | }
848 | })
849 | this._headerHeight = headerHeight
850 |
851 | if (timestamp - this._beginResetScrollTopTimeStamp > scrollBounceAnimationDuration) {
852 | this._beginResetScrollTopTimeStamp = null
853 | this._moveMent = 0
854 |
855 | if (this._refreshState != refreshing) {
856 | if (renderSectionHeader && listSectionProps) {
857 | this._floatSectionHeader.setState({
858 | hidden: false,
859 | })
860 | }
861 | }
862 | else {
863 | //console.log(`_resetRefreshScrollTop onRefresh()`)
864 | this.props.onRefresh && this.props.onRefresh()
865 | }
866 | this._refreshingHeaderHeight = headerHeight
867 |
868 | return
869 | }
870 |
871 | this._refreshBackAnimationFrame = this.requestAnimationFrame(this._resetRefreshScrollTop)
872 | }
873 |
874 | _resetLoadMoreScrollTop = (timestamp) => {
875 | let {pullUpStayDistance} = this.props
876 | let {loading_more} = viewState
877 | if (this._loadMoreState != loading_more) {
878 | pullUpStayDistance = 0
879 | }
880 | let footerHeight, scrollViewTranslateY
881 | if (!this._beginResetScrollTopTimeStamp) {
882 | footerHeight = Math.abs(this._moveMent)
883 | scrollViewTranslateY = 0
884 | this._beginResetScrollTopTimeStamp = timestamp
885 | this._fixedScrollY = this._scrollY
886 | }
887 | else {
888 | let scrollViewTranslateMaxY
889 | let percent = (timestamp - this._beginResetScrollTopTimeStamp) / scrollBounceAnimationDuration
890 | footerHeight = Math.abs(this._moveMent) - (Math.abs(this._moveMent) - pullUpStayDistance) * easeOutCirc(percent, scrollBounceAnimationDuration * percent, 0, 1, scrollBounceAnimationDuration)
891 | scrollViewTranslateMaxY = this._fixedScrollY - (this._scrollViewContentHeight - this._scrollViewContainerHeight)
892 | scrollViewTranslateY = scrollViewTranslateMaxY * (timestamp - this._beginResetScrollTopTimeStamp) / refreshAnimationDuration
893 | if (scrollViewTranslateY > scrollViewTranslateMaxY) {
894 | scrollViewTranslateY = scrollViewTranslateMaxY
895 | }
896 | }
897 |
898 | if (footerHeight < pullUpStayDistance) {
899 | footerHeight = pullUpStayDistance
900 | }
901 |
902 | this._footer.setNativeProps({
903 | style: {
904 | height: footerHeight,
905 | }
906 | })
907 | this._scrollView.scrollTo({ y: this._fixedScrollY - scrollViewTranslateY, animated: false, })
908 |
909 | if (timestamp - this._beginResetScrollTopTimeStamp > scrollBounceAnimationDuration) {
910 | this._beginResetScrollTopTimeStamp = null
911 | this._moveMent = 0
912 |
913 | if (this._loadMoreState == loading_more) {
914 | this.props.onLoadMore && this.props.onLoadMore()
915 | }
916 |
917 | return
918 | }
919 |
920 | this._refreshBackAnimationFrame = this.requestAnimationFrame(this._resetLoadMoreScrollTop)
921 | }
922 |
923 | _resetHeaderLayout = (timestamp) => {
924 | let {loaded_all, load_more_none} = viewState
925 | let {pullDownStayDistance, dataSource, renderSectionHeader, listSectionProps, } = this.props
926 | let headerHeight
927 | if (!this._beginTimeStamp) {
928 | headerHeight = pullDownStayDistance
929 | this._beginTimeStamp = timestamp
930 | this._fixedScrollY = this._scrollY > 0 ? this._scrollY : 0
931 | }
932 | else {
933 | headerHeight = pullDownStayDistance - (pullDownStayDistance - this._fixedScrollY - StyleSheet.hairlineWidth) * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
934 | //if (headerHeight < 0) {
935 | // headerHeight = 0
936 | //}
937 | /**
938 | * fix the bug that onContentSizeChange sometimes is not triggered, it causes incorrect contentHeight(this._scrollViewContentHeight)
939 | */
940 | if (headerHeight < StyleSheet.hairlineWidth) {
941 | headerHeight = StyleSheet.hairlineWidth
942 | }
943 | }
944 | this._header.setNativeProps({
945 | style: {
946 | height: headerHeight,
947 | }
948 | })
949 | this._headerHeight = headerHeight
950 |
951 | if (timestamp - this._beginTimeStamp > refreshAnimationDuration) {
952 |
953 | this._header.setNativeProps({
954 | style: {
955 | height: 0,
956 | }
957 | })
958 | this._headerHeight = 0
959 |
960 | if (this._fixedScrollY > 0) {
961 | this._scrollView.scrollTo({ y: 0, animated: false, })
962 | }
963 | this._beginTimeStamp = null
964 | this._refreshBackAnimating = false
965 | this._afterRefreshBacked = true
966 |
967 | this._setPaddingBlank()
968 |
969 | ////force show footer
970 | //this._footer.setNativeProps({
971 | // style: {
972 | // opacity: 1,
973 | // }
974 | //})
975 |
976 | ////enabled swipe event
977 | //this._swipeRefreshLayout.setNativeProps({
978 | // refreshing: false,
979 | //})
980 | //this._scrollView.setNativeProps({
981 | // scrollEnabled: true,
982 | //})
983 |
984 | //reset loadMoreState to load_more_none
985 | if (this._loadMoreState == loaded_all) {
986 | this._loadMoreState = load_more_none
987 | this._footer.setState({
988 | pullState: this._loadMoreState,
989 | pullDistancePercent: 0,
990 | })
991 | }
992 |
993 | //enabled swipe event
994 | this._swipeRefreshLayout.setNativeProps({
995 | refreshing: false,
996 | })
997 | this._scrollView.setNativeProps({
998 | scrollEnabled: true,
999 | })
1000 |
1001 | if (renderSectionHeader && listSectionProps) {
1002 | this._floatSectionHeader.setState({
1003 | hidden: false,
1004 | })
1005 | let firstSectionID = dataSource.sectionIdentities[ 0 ]
1006 | //reset float section header
1007 | this._floatSectionHeader.setSectionID(firstSectionID)
1008 | }
1009 | return
1010 | }
1011 |
1012 | this.requestAnimationFrame(this._resetHeaderLayout)
1013 | }
1014 |
1015 | _resetFooterLayout = (timestamp) => {
1016 | let {pullUpStayDistance} = this.props
1017 | let footerHeight, scrollViewTranslateY
1018 | if (!this._beginTimeStamp) {
1019 | footerHeight = pullUpStayDistance
1020 | scrollViewTranslateY = 0
1021 | this._beginTimeStamp = timestamp
1022 | this._fixedScrollY = this._scrollY
1023 | }
1024 | else {
1025 | let scrollViewTranslateMaxY
1026 | footerHeight = pullUpStayDistance - (pullUpStayDistance - StyleSheet.hairlineWidth) * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
1027 |
1028 | if (this._touching && (this._fixedScrollY - (this._scrollViewContentHeight - this._scrollViewContainerHeight)) > pullUpStayDistance) {
1029 | scrollViewTranslateMaxY = pullUpStayDistance
1030 | }
1031 | else {
1032 | scrollViewTranslateMaxY = this._fixedScrollY - (this._scrollViewContentHeight - this._scrollViewContainerHeight)
1033 | }
1034 | scrollViewTranslateY = (scrollViewTranslateMaxY - StyleSheet.hairlineWidth) * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
1035 | //if (footerHeight < 0) {
1036 | // footerHeight = 0
1037 | //}
1038 | //if (scrollViewTranslateY > scrollViewTranslateMaxY) {
1039 | // scrollViewTranslateY = scrollViewTranslateMaxY
1040 | //}
1041 | /**
1042 | * fix the bug that onContentSizeChange sometimes is not triggered, it causes incorrect contentHeight(this._scrollViewContentHeight)
1043 | */
1044 | if (footerHeight < StyleSheet.hairlineWidth) {
1045 | footerHeight = StyleSheet.hairlineWidth
1046 | }
1047 | if (scrollViewTranslateY > scrollViewTranslateMaxY - StyleSheet.hairlineWidth) {
1048 | scrollViewTranslateY = scrollViewTranslateMaxY - StyleSheet.hairlineWidth
1049 | }
1050 | }
1051 |
1052 | this._footer.setNativeProps({
1053 | style: {
1054 | height: footerHeight,
1055 | }
1056 | })
1057 |
1058 | this._scrollView.scrollTo({ y: this._fixedScrollY - scrollViewTranslateY, animated: false, })
1059 |
1060 | if (timestamp - this._beginTimeStamp > refreshAnimationDuration) {
1061 |
1062 | this._footer.setNativeProps({
1063 | style: {
1064 | height: 0,
1065 | }
1066 | })
1067 | this._scrollView.scrollTo({
1068 | y: this._fixedScrollY - scrollViewTranslateY + StyleSheet.hairlineWidth,
1069 | animated: false,
1070 | })
1071 |
1072 | this._beginTimeStamp = null
1073 | this._loadMoreBackAnimating = false
1074 | this._afterLoadMoreBacked = true
1075 |
1076 | this._setPaddingBlank()
1077 |
1078 | //enabled swipe event
1079 | this._swipeRefreshLayout.setNativeProps({
1080 | refreshing: false,
1081 | })
1082 | this._scrollView.setNativeProps({
1083 | scrollEnabled: true,
1084 | })
1085 |
1086 | return
1087 | }
1088 |
1089 | this.requestAnimationFrame(this._resetFooterLayout)
1090 | }
1091 |
1092 | _renderHeader = () => {
1093 | return (
1094 | this._header = component }
1095 | style={[styles.header, styles.shrink,]}
1096 | viewType={refreshViewType.header}
1097 | renderRefreshContent={this.props.renderHeader}/>
1098 | )
1099 | }
1100 |
1101 | _renderFooter = () => {
1102 | return (
1103 | this._footer = component }
1104 | onLayout={this._onFooterLayout}
1105 | style={[styles.footer, this.props.autoLoadMore ? null : styles.shrink, { opacity: 0, }, ]}
1106 | viewType={refreshViewType.footer}
1107 | renderRefreshContent={this.props.renderFooter}/>
1108 | )
1109 | }
1110 |
1111 | _onFooterLayout = (e) => {
1112 | this._autoLoadFooterHeight = e.nativeEvent.layout.height
1113 | //console.log(`_onFooterLayout this._autoLoadFooterHeight = ${this._autoLoadFooterHeight}`)
1114 | }
1115 |
1116 | //only used by listview for android
1117 | _renderSectionHeader = (sectionData, sectionID) => {
1118 | let {listSectionProps, renderSectionHeader, renderRowWithVisibility} = this.props
1119 |
1120 | if (!renderSectionHeader) {
1121 | return null
1122 | }
1123 |
1124 | if (listSectionProps) {
1125 | if (renderRowWithVisibility) {
1126 | return (
1127 | this._listSectionRefs[sectionID] = component }
1128 | {...listSectionProps}
1129 | renderChildren={renderSectionHeader.bind(this, sectionData, sectionID)}/>
1130 | )
1131 | }
1132 | else {
1133 | return (
1134 | this._listSectionRefs[sectionID] = component }
1135 | {...listSectionProps}>
1136 | {renderSectionHeader(sectionData, sectionID)}
1137 |
1138 | )
1139 | }
1140 | }
1141 | else {
1142 | return renderSectionHeader(sectionData, sectionID)
1143 | }
1144 | }
1145 |
1146 | //only used by listview
1147 | _renderRow = (rowData, sectionID, rowID) => {
1148 | let {listItemProps, renderRow, renderRowWithVisibility} = this.props
1149 |
1150 | if (listItemProps) {
1151 | if (renderRowWithVisibility) {
1152 | return (
1153 | this._listItemRefs[sectionID + keySeparator + rowID] = {sectionID, component,} }
1154 | {...listItemProps}
1155 | renderChildren={renderRow.bind(this, rowData, sectionID, rowID)}/>
1156 | )
1157 | }
1158 | else {
1159 | return (
1160 | this._listItemRefs[sectionID + keySeparator + rowID] = {sectionID, component,} }
1161 | {...listItemProps}>
1162 | {renderRow(rowData, sectionID, rowID)}
1163 |
1164 | )
1165 | }
1166 | }
1167 | else {
1168 | return renderRow(rowData, sectionID, rowID)
1169 | }
1170 | }
1171 |
1172 | clearListRowRefsCache = () => {
1173 | this._listSectionRefs = {}
1174 | this._listItemRefs = {}
1175 | }
1176 |
1177 | }
1178 |
1179 | export default TimerEnhance(PullToRefreshListView)
1180 |
--------------------------------------------------------------------------------
/PullToRefreshListView-ios.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A smart pull-down-refresh and pull-up-loadmore react-native listview
3 | * https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview/
4 | * Released under the MIT license
5 | * Copyright (c) 2016 react-native-component
6 | */
7 |
8 | import React, {
9 | Component,
10 | } from 'react'
11 | import PropTypes from 'prop-types';
12 | import {
13 | View,
14 | ScrollView,
15 | ListView,
16 | StyleSheet,
17 | Text,
18 | Platform,
19 | } from 'react-native'
20 |
21 | //import TimerEnhance from '../react-native-smart-timer-enhance'
22 | import TimerEnhance from 'react-native-smart-timer-enhance'
23 | import { withinErrorMargin, } from './utils'
24 | import constants, {
25 | viewType,
26 | viewState,
27 | refreshViewType,
28 | refreshAnimationDuration,
29 | scrollBounceAnimationDuration,
30 | } from './constants'
31 | import { easeOutCirc, } from './easing'
32 | import RefreshView from './RefreshView'
33 | import ListItem from './ListItem'
34 |
35 | const styles = StyleSheet.create({
36 | header: {
37 | justifyContent: 'flex-end',
38 | },
39 | footer: {
40 | justifyContent: 'flex-start',
41 | },
42 | shrink: {
43 | height: 0,
44 | },
45 | marginVertical: {
46 | marginTop: 0,
47 | marginBottom: 0,
48 | marginVertical: 0,
49 | },
50 | paddingVertical: {
51 | paddingTop: 0,
52 | paddingBottom: 0,
53 | paddingVertical: 0,
54 | }
55 | })
56 |
57 | class PullToRefreshListView extends Component {
58 |
59 | static constants = constants
60 |
61 | static defaultProps = {
62 | viewType: viewType.scrollView,
63 | pullUpDistance: 50,
64 | pullUpStayDistance: 35,
65 | pullDownDistance: 50,
66 | pullDownStayDistance: 35,
67 | enabledPullUp: true,
68 | enabledPullDown: true,
69 | autoLoadMore: false,
70 | scrollEventThrottle: 16,
71 | dataSource: new ListView.DataSource({
72 | rowHasChanged: (r1, r2) => r1 !== r2,
73 | }),
74 | renderRow: () => null,
75 | renderScrollComponent: props => ,
76 | onEndReachedThreshold: StyleSheet.hairlineWidth, //0,
77 | initialListSize: 10,
78 | stickyHeaderIndices: [],
79 | pageSize: 1,
80 | scrollRenderAheadDistance: 1000,
81 | }
82 |
83 | static propTypes = {
84 | ...ListView.propTypes,
85 | listItemProps: PropTypes.shape(View.propTypes),
86 | viewType: PropTypes.oneOf([
87 | viewType.scrollView,
88 | viewType.listView,
89 | ]),
90 | pullUpDistance: PropTypes.number,
91 | pullUpStayDistance: PropTypes.number,
92 | pullDownDistance: PropTypes.number,
93 | pullDownStayDistance: PropTypes.number,
94 | enabledPullUp: PropTypes.bool,
95 | enabledPullDown: PropTypes.bool,
96 | autoLoadMore: PropTypes.bool,
97 | onRefresh: PropTypes.func,
98 | onLoadMore: PropTypes.func,
99 | }
100 |
101 | constructor (props) {
102 | super(props)
103 | this.state = {}
104 | let {refresh_none, load_more_none} = viewState
105 |
106 | if (props.autoLoadMore && props.viewType == viewType.listView) {
107 | this._onEndReached = () => {
108 | let { refreshing, load_more_none, loading_more,} = viewState
109 | //if (this._refreshState != refreshing && this._loadMoreState == load_more_none) {
110 | if (this._canLoadMore && this._refreshState != refreshing && this._loadMoreState == load_more_none) {
111 | this._loadMoreState = loading_more
112 | this._footer.setState({
113 | pullState: this._loadMoreState,
114 | })
115 |
116 | props.onLoadMore && props.onLoadMore()
117 | }
118 | }
119 | }
120 |
121 | /**
122 | * (occurs on react-native 0.32, and maybe also occurs on other versions)ListView renderHeader/renderFooter => View's children cannot be visible when parent's height < StyleSheet.hairlineWidth
123 | * ScrollView does not exist this strange bug
124 | */
125 | this._fixedBoundary = !props.autoLoadMore && props.viewType == viewType.scrollView ? 0 : StyleSheet.hairlineWidth
126 |
127 | this._refreshState = refresh_none
128 | this._loadMoreState = load_more_none
129 | this._refreshBackAnimating = false
130 | this._loadMoreBackAnimating = false
131 | this._afterRefreshBacked = false
132 | this._afterLoadMoreBacked = false
133 | this._beginTimeStamp = null
134 | this._beginResetScrollTopTimeStamp = null
135 | this._refreshBackAnimationFrame = null
136 | this._touching = false
137 | this._scrollY = 0
138 | this._lastScrollY = 0
139 | this._fixedScrollY = 0
140 | this._refreshFixScrollY = 0
141 | this._paddingBlankDistance = 0
142 |
143 | this._listItemRefs = {}
144 |
145 | this._headerHeight = 0
146 | this._canLoadMore = false
147 | this._autoLoadFooterHeight = 0
148 | this._onRefreshed = false
149 | }
150 |
151 | render () {
152 | return (
153 | this.props.viewType == viewType.scrollView ?
154 | this._scrollView = component }
156 | {...this.props}
157 | style={[this.props.style, styles.paddingVertical,]}
158 | contentContainerStyle={[this.props.contentContainerStyle, styles.marginVertical,]}
159 | onLayout={this._onLayout}
160 | onContentSizeChange={this._onContentSizeChange}
161 | onResponderGrant={this._onResponderGrant}
162 | onScroll={this._onScroll}
163 | onMomentumScrollBegin={this._onResponderRelease}>
164 | {this._renderHeader()}
165 | {this.props.children}
166 | {this._renderFooter()}
167 | :
168 | this._scrollView = component }
170 | {...this.props}
171 | style={[this.props.style, styles.paddingVertical,]}
172 | contentContainerStyle={[this.props.contentContainerStyle, styles.marginVertical,]}
173 | onEndReached={this._onEndReached}
174 | onLayout={this._onLayout}
175 | onContentSizeChange={this._onContentSizeChange}
176 | onResponderGrant={this._onResponderGrant}
177 | onScroll={this._onScroll}
178 | onMomentumScrollBegin={this._onResponderRelease}
179 | onChangeVisibleRows={this._onChangeVisibleRows}
180 | listItemProps={this.props.listItemProps}
181 | renderRow={this._renderRow}
182 | renderHeader={this._renderHeader}
183 | renderFooter={this._renderFooter}
184 | renderScrollComponent={ props => this._innerScrollView = component } {...props} /> }/>
185 |
186 | )
187 | }
188 |
189 | componentDidMount () {
190 | /**
191 | * (occurs on react-native 0.32, and maybe also occurs on react-native 0.30+)ListView renderHeader/renderFooter => View's children cannot be visible when parent's height < StyleSheet.hairlineWidth
192 | * ScrollView does not exist this strange bug
193 | */
194 | if (this.props.viewType == viewType.listView) {
195 | this._header.setNativeProps({
196 | style: {
197 | height: this._fixedBoundary
198 | }
199 | })
200 | this._headerHeight = this._fixedBoundary
201 |
202 | if (!this.props.autoLoadMore) {
203 | this._footer.setNativeProps({
204 | style: {
205 | height: this._fixedBoundary
206 | }
207 | })
208 | }
209 | }
210 | }
211 |
212 | setNativeProps = (props) => {
213 | this._scrollView.setNativeProps(props)
214 | }
215 |
216 | beginRefresh = (bounceDisabled) => {
217 | this._scrollView.setNativeProps({
218 | scrollEnabled: false
219 | })
220 | //this.requestAnimationFrame(this._resetReverseHeaderLayout)
221 | if (!bounceDisabled) {
222 | this.requestAnimationFrame(this._resetReverseHeaderLayout)
223 | }
224 | else {
225 | this.props.onRefresh && this.props.onRefresh()
226 | }
227 | let {refreshing,} = viewState
228 | this._refreshState = refreshing
229 | this._header.setState({
230 | pullState: this._refreshState,
231 | pullDistancePercent: 1,
232 | })
233 |
234 | //force hide footer
235 | this._footer.setNativeProps({
236 | style: {
237 | opacity: 0,
238 | }
239 | })
240 |
241 | //this.props.onRefresh && this.props.onRefresh() //move to _resetReverseHeaderLayout and _resetRefreshScrollTop
242 | //this._listItemRefs = {}
243 | }
244 |
245 | endRefresh = (bounceDisabled) => {
246 | this._onRefreshed = false
247 | if(!bounceDisabled) {
248 | this._canLoadMore = false
249 | }
250 |
251 | //this._scrollView.setNativeProps({
252 | // scrollEnabled: false
253 | //})
254 | let {refresh_none, loaded_all, load_more_none} = viewState
255 | let {pullDownStayDistance} = this.props
256 | this._refreshState = refresh_none
257 | this._header.setState({
258 | pullState: this._refreshState,
259 | })
260 |
261 | this._refreshBackAnimating = true
262 |
263 | //if (this._scrollY < pullDownStayDistance) {
264 | if (!bounceDisabled && this._scrollY < pullDownStayDistance) {
265 | this.requestAnimationFrame(this._resetHeaderLayout)
266 | }
267 | else {
268 | this._header.setNativeProps({
269 | style: {
270 | //height: 0,
271 | /**
272 | * (occurs on react-native 0.32, and maybe also occurs on react-native 0.30+)ListView renderHeader/renderFooter => View's children cannot be visible when parent's height < StyleSheet.hairlineWidth
273 | * ScrollView does not exist this strange bug
274 | */
275 | height: this._fixedBoundary,
276 | }
277 | })
278 | this._headerHeight = this._fixedBoundary
279 |
280 | //this._scrollView.scrollTo({ y: this._scrollY - pullDownStayDistance + this._fixedBoundary, animated: false, })
281 | if (!bounceDisabled) {
282 | //console.log(`direct endRefresh -> this._scrollY = ${this._scrollY}`)
283 | this._scrollView.scrollTo({
284 | y: this._scrollY - pullDownStayDistance + this._fixedBoundary,
285 | animated: false,
286 | })
287 | }
288 | this._beginTimeStamp = null
289 | this._refreshBackAnimating = false
290 | this._afterRefreshBacked = true
291 |
292 | this._afterDirectRefresh = true
293 |
294 | //force show footer
295 | this._footer.setNativeProps({
296 | style: {
297 | opacity: 1,
298 | }
299 | })
300 |
301 | //this._setPaddingBlank()
302 | //this._setPaddingBlank(bounceDisabled)
303 |
304 | //reset loadMoreState to load_more_none
305 | if (this._loadMoreState == loaded_all) {
306 | this._loadMoreState = load_more_none
307 | this._footer.setState({
308 | pullState: this._loadMoreState,
309 | pullDistancePercent: 0,
310 | })
311 | }
312 |
313 | this._scrollView.setNativeProps({
314 | scrollEnabled: true
315 | })
316 | }
317 | }
318 |
319 | endLoadMore = (loadedAll) => {
320 | //this._scrollView.setNativeProps({
321 | // scrollEnabled: false
322 | //})
323 |
324 | let {load_more_none, loaded_all} = viewState
325 | let {autoLoadMore} = this.props
326 | if (!loadedAll) {
327 | this._loadMoreState = load_more_none
328 | }
329 | else {
330 | this._loadMoreState = loaded_all
331 | }
332 | this._footer.setState({
333 | pullState: this._loadMoreState,
334 | })
335 |
336 | if (!autoLoadMore) {
337 | this._loadMoreBackAnimating = true
338 |
339 | if (this._scrollY >= this._scrollViewContentHeight - this._scrollViewContainerHeight) {
340 | this.requestAnimationFrame(this._resetFooterLayout)
341 | }
342 | else {
343 | this._footer.setNativeProps({
344 | style: {
345 | //height: 0,
346 | /**
347 | * (occurs on react-native 0.32, and maybe also occurs on react-native 0.30+)ListView renderHeader/renderFooter => View's children cannot be visible when parent's height < StyleSheet.hairlineWidth
348 | * ScrollView does not exist this strange bug
349 | */
350 | height: this._fixedBoundary,
351 | }
352 | })
353 | this._scrollView.scrollTo({ y: this._scrollY, animated: false, })
354 |
355 | this._beginTimeStamp = null
356 | this._loadMoreBackAnimating = false
357 | this._afterLoadMoreBacked = true
358 |
359 | this._setPaddingBlank()
360 |
361 | this._scrollView.setNativeProps({
362 | scrollEnabled: true,
363 | })
364 | }
365 | }
366 | else {
367 | this._setPaddingBlank()
368 |
369 | this._scrollView.setNativeProps({
370 | scrollEnabled: true,
371 | })
372 | }
373 | }
374 |
375 | _setPaddingBlank = (paddingDisabled) => {
376 | let innerViewRef = this._scrollView.refs.InnerScrollView || this._scrollView._innerViewRef || this._innerScrollView.refs.InnerScrollView || this._innerScrollView._innerViewRef
377 | innerViewRef.measure((ox, oy, width, height, px, py) => {
378 |
379 | let opacity
380 | let footerHeight = this.props.autoLoadMore ? this._autoLoadFooterHeight : 0
381 | //console.log(`footerHeight = ${footerHeight}, height = ${height}, this._paddingBlankDistance = ${this._paddingBlankDistance}, this._headerHeight = ${this._headerHeight}, this._scrollViewContainerHeight = ${this._scrollViewContainerHeight},`)
382 | if(height - this._paddingBlankDistance - this._headerHeight - footerHeight < this._scrollViewContainerHeight) {
383 | opacity = 0
384 | this._canLoadMore = false
385 | }
386 | else {
387 | if(!this._afterDirectRefresh) {
388 | opacity = 1
389 | this._canLoadMore = true
390 | }
391 | else {
392 | opacity = 0
393 | this._canLoadMore = false
394 | }
395 |
396 | }
397 |
398 | if (!paddingDisabled && height - this._paddingBlankDistance < this._scrollViewContainerHeight) {
399 | this._paddingBlankDistance = this._scrollViewContainerHeight - (height - this._paddingBlankDistance)
400 | }
401 | else {
402 | this._paddingBlankDistance = 0
403 | }
404 |
405 | //this._footer.setNativeProps({
406 | // style: {
407 | // marginTop: this._paddingBlankDistance,
408 | // }
409 | //})
410 | /**
411 | * (occurs on react-native 0.32, and maybe also occurs on react-native 0.30+)ListView renderHeader/renderFooter => View's children cannot be visible when parent's height < StyleSheet.hairlineWidth
412 | * ScrollView does not exist this strange bug
413 | */
414 | let {autoLoadMore, enabledPullUp, enabledPullDown, } = this.props
415 | let ratio = 1 //always includes PullDown
416 | if (enabledPullUp && !autoLoadMore) {
417 | ratio++
418 | }
419 |
420 | if(!this._footer) {
421 | return
422 | }
423 |
424 | //console.log(`opacity = ${opacity}, this._canLoadMore = ${this._canLoadMore}, this._paddingBlankDistance = ${this._paddingBlankDistance}, this._afterDirectRefresh = ${this._afterDirectRefresh}`)
425 |
426 | /* (occurs on react-native 0.39, and maybe also occurs on other versions)ListView renderFooter => View's children cannot be visible when parent's marginTop < 0 */
427 | let marginTop = this._paddingBlankDistance - this._fixedBoundary * ratio
428 | if(marginTop < 0) {
429 | marginTop = 0
430 | }
431 | this._footer.setNativeProps({
432 | style: {
433 | opacity,
434 | marginTop,
435 | }
436 | })
437 | })
438 | }
439 |
440 | _onLayout = (e) => {
441 | if (this._scrollViewContainerHeight == null) {
442 | this._scrollViewContainerHeight = e.nativeEvent.layout.height
443 | }
444 |
445 | this._setPaddingBlank()
446 |
447 | this.props.onLayout && this.props.onLayout(e)
448 | }
449 |
450 | //ensure that onContentSizeChange must be triggered while ending resetHeaderLayout/resetFooterLayout animation
451 | _onContentSizeChange = (contentWidth, contentHeight) => {
452 | let {refreshing, loading_more} = viewState
453 | if (this._scrollViewContentHeight == null
454 | || ((this._refreshState != refreshing && !this._refreshBackAnimating)
455 | //&& (this._loadMoreState != loading_more && !this._loadMoreBackAnimating))) {
456 | && ( this.props.autoLoadMore || (!this.props.autoLoadMore && this._loadMoreState != loading_more && !this._loadMoreBackAnimating) ))) {
457 | this._scrollViewContentHeight = contentHeight
458 |
459 | if (this._afterDirectRefresh) {
460 | this._afterDirectRefresh = false
461 |
462 | let {pullDownStayDistance} = this.props
463 |
464 | if (this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight + pullDownStayDistance) {
465 | let y = this._scrollViewContentHeight - this._scrollViewContainerHeight
466 | y = y > 0 ? y : 0
467 | this._scrollView.scrollTo({
468 | y,
469 | animated: false,
470 | })
471 | //console.log(`_onContentSizeChange y = ${y} this._scrollY = ${this._scrollY} this._scrollViewContentHeight = ${this._scrollViewContentHeight}, this._scrollViewContainerHeight = ${this._scrollViewContainerHeight}`)
472 | }
473 |
474 | this._setPaddingBlank()
475 | }
476 | }
477 |
478 | this.props.onContentSizeChange && this.props.onContentSizeChange(contentWidth, contentHeight)
479 | }
480 |
481 | _onResponderGrant = (e) => {
482 | this._touching = true
483 |
484 | if (this._refreshBackAnimationFrame) {
485 | this.cancelAnimationFrame(this._refreshBackAnimationFrame)
486 | this._refreshBackAnimationFrame = null
487 | this._beginResetScrollTopTimeStamp = null
488 |
489 | if(!this._onRefreshed) {
490 | this._onRefreshed = true
491 | this.props.onRefresh && this.props.onRefresh()
492 | }
493 | }
494 |
495 | if (this._afterRefreshBacked) {
496 | this._afterRefreshBacked = false
497 | }
498 | if (this._afterLoadMoreBacked) {
499 | this._afterLoadMoreBacked = false
500 | }
501 |
502 | if (e.nativeEvent.contentOffset) {
503 | this._scrollY = e.nativeEvent.contentOffset.y
504 | this._lastScrollY = this._scrollY
505 | }
506 |
507 | let {refresh_idle, refreshing, load_more_idle, loading_more, loaded_all} = viewState
508 | let {enabledPullUp, enabledPullDown, pullUpDistance, pullDownDistance, autoLoadMore, } = this.props
509 | if (enabledPullDown && this._refreshState != refreshing && this._loadMoreState != loading_more && this._scrollY < 0) {
510 | this._refreshState = refresh_idle
511 | this._header.setState({
512 | pullState: this._refreshState,
513 | pullDistancePercent: -this._scrollY / pullDownDistance,
514 | })
515 | }
516 | else {
517 | if (this._canLoadMore && !autoLoadMore && enabledPullUp && this._refreshState != refreshing && this._loadMoreState != loading_more && this._loadMoreState != loaded_all && this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight) {
518 | this._loadMoreState = load_more_idle
519 | this._footer.setState({
520 | pullState: this._loadMoreState,
521 | pullDistancePercent: (this._scrollY - this._scrollViewContentHeight + this._scrollViewContainerHeight) / pullUpDistance,
522 | })
523 | }
524 | }
525 | }
526 |
527 | _onScroll = (e) => {
528 | let {refresh_none, refresh_idle, will_refresh, refreshing,
529 | load_more_none, load_more_idle, will_load_more, loading_more, loaded_all,} = viewState
530 | let {pullUpDistance, pullDownDistance, autoLoadMore, enabledPullUp, enabledPullDown, } = this.props
531 | this._scrollY = e.nativeEvent.contentOffset.y
532 | //console.log(`this._scrollY = ${this._scrollY}`)
533 |
534 | if (this._scrollY < this._lastScrollY) {
535 | if (this._refreshState == refresh_none && !this._refreshBackAnimating && !this._afterRefreshBacked) {
536 | if (enabledPullDown && this._refreshState != refreshing && this._loadMoreState != loading_more && this._scrollY < 0) {
537 | this._refreshState = refresh_idle
538 | this._header.setState({
539 | pullState: this._refreshState,
540 | pullDistancePercent: -this._scrollY / pullDownDistance,
541 | })
542 | }
543 | }
544 | }
545 | else {
546 | if (this._loadMoreState == load_more_none && !this._loadMoreBackAnimating && !this._afterLoadMoreBacked) {
547 | if (this._canLoadMore && enabledPullUp && !autoLoadMore && this._refreshState != refreshing && this._loadMoreState != loading_more && this._loadMoreState != loaded_all && this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight) {
548 | this._loadMoreState = load_more_idle
549 | this._footer.setState({
550 | pullState: this._loadMoreState,
551 | pullDistancePercent: (this._scrollY - this._scrollViewContentHeight + this._scrollViewContainerHeight) / pullUpDistance,
552 | })
553 | }
554 | }
555 | }
556 |
557 | if (this._scrollY < 0) {
558 | if (this._refreshState == refresh_idle || this._refreshState == will_refresh) {
559 | if (-this._scrollY >= pullDownDistance) {
560 | if (this._refreshState == refresh_idle) {
561 | this._refreshState = will_refresh
562 | }
563 | }
564 | else {
565 | if (this._refreshState == will_refresh) {
566 | this._refreshState = refresh_idle
567 | }
568 | }
569 | this._header.setState({
570 | pullState: this._refreshState,
571 | pullDistancePercent: -this._scrollY / pullDownDistance,
572 | })
573 | }
574 | }
575 | else if (!autoLoadMore && this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight) {
576 | if (this._loadMoreState == load_more_idle || this._loadMoreState == will_load_more) {
577 | if (this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight + pullUpDistance) {
578 | if (this._loadMoreState == load_more_idle) {
579 | this._loadMoreState = will_load_more
580 | }
581 | }
582 | else {
583 | if (this._loadMoreState == will_load_more) {
584 | this._loadMoreState = load_more_idle
585 | }
586 | }
587 | this._footer.setState({
588 | pullState: this._loadMoreState,
589 | pullDistancePercent: (this._scrollY - this._scrollViewContentHeight + this._scrollViewContainerHeight) / pullUpDistance,
590 | })
591 | }
592 | }
593 | else {
594 | if (this._scrollY == 0) {
595 | if (this._refreshState == refresh_idle) {
596 | this._refreshState = refresh_none
597 | this._header.setState({
598 | pullState: this._refreshState,
599 | })
600 |
601 | }
602 | }
603 | else {
604 | if (!autoLoadMore) {
605 | if (withinErrorMargin(this._scrollY, this._scrollViewContentHeight - this._scrollViewContainerHeight)) {
606 | if (this._loadMoreState == load_more_idle) {
607 | this._loadMoreState = load_more_none
608 | this._footer.setState({
609 | pullState: this._loadMoreState,
610 | })
611 | }
612 | }
613 | }
614 | else {
615 | //use onEndReached handler when viewType is 'listView' to fix double triggering onLoadMore sometimes, but no idea when viewType is 'scrollView'
616 | if (this.props.viewType == viewType.scrollView) {
617 | if (withinErrorMargin(this._scrollY, this._scrollViewContentHeight - this._scrollViewContainerHeight, this.props.onEndReachedThreshold)
618 | || this._scrollY > this._scrollViewContentHeight - this._scrollViewContainerHeight - this.props.onEndReachedThreshold) {
619 | if (this._refreshState != refreshing && this._loadMoreState == load_more_none) {
620 | this._loadMoreState = loading_more
621 | this._footer.setState({
622 | pullState: this._loadMoreState,
623 | })
624 |
625 | this.props.onLoadMore && this.props.onLoadMore()
626 | }
627 | }
628 | }
629 | }
630 | }
631 | }
632 |
633 | this._lastScrollY = this._scrollY
634 |
635 | this.props.onScroll && this.props.onScroll(e)
636 | }
637 |
638 | _onResponderRelease = (e) => {
639 | this._touching = false
640 |
641 | let {will_refresh, refreshing, will_load_more, loading_more} = viewState
642 |
643 | if (this._loadMoreState != loading_more && this._refreshState == will_refresh && !this._refreshBackAnimating && !this._afterRefreshBacked) {
644 | let {pullDownStayDistance} = this.props
645 | this._refreshFixScrollY = this._scrollY + pullDownStayDistance
646 | this._header.setNativeProps({
647 | style: {
648 | height: pullDownStayDistance,
649 | }
650 | })
651 | this._headerHeight = pullDownStayDistance
652 |
653 | this._scrollView.scrollTo({ y: this._refreshFixScrollY, animated: false, })
654 | this._refreshBackAnimationFrame = this.requestAnimationFrame(this._resetRefreshScrollTop)
655 | this._refreshState = refreshing
656 | this._header.setState({
657 | pullState: this._refreshState,
658 | pullDistancePercent: 0,
659 | })
660 |
661 | //this.props.onRefresh && this.props.onRefresh() //move to _resetReverseHeaderLayout and _resetRefreshScrollTop
662 | //this._listItemRefs = {}
663 | }
664 | else {
665 | if (this._refreshState != refreshing && this._loadMoreState == will_load_more && !this._loadMoreBackAnimating && !this._afterLoadMoreBacked) {
666 | let {pullUpStayDistance} = this.props
667 | this._footer.setNativeProps({
668 | style: {
669 | height: pullUpStayDistance,
670 | }
671 | })
672 | this._loadMoreState = loading_more
673 | this._footer.setState({
674 | pullState: this._loadMoreState,
675 | pullDistancePercent: 0,
676 | })
677 |
678 | this.props.onLoadMore && this.props.onLoadMore()
679 | }
680 | }
681 | }
682 |
683 | _resetRefreshScrollTop = (timestamp) => {
684 | if (!this._beginResetScrollTopTimeStamp) {
685 | this._beginResetScrollTopTimeStamp = timestamp
686 | this._scrollView.scrollTo({ y: this._refreshFixScrollY, animated: false, })
687 | }
688 | else {
689 | let percent = (timestamp - this._beginResetScrollTopTimeStamp) / scrollBounceAnimationDuration
690 | let resetScrollTop
691 | resetScrollTop = -this._refreshFixScrollY - -this._refreshFixScrollY * easeOutCirc(percent, scrollBounceAnimationDuration * percent, 0, 1, scrollBounceAnimationDuration)
692 | if (resetScrollTop < 0) {
693 | resetScrollTop = 0
694 | }
695 | this._scrollView.scrollTo({ y: -resetScrollTop, animated: false, })
696 | if (timestamp - this._beginResetScrollTopTimeStamp > scrollBounceAnimationDuration) {
697 | this._refreshBackAnimationFrame = null
698 | this._beginResetScrollTopTimeStamp = null
699 |
700 | if(!this._onRefreshed) {
701 | this._onRefreshed = true
702 | this.props.onRefresh && this.props.onRefresh()
703 | }
704 |
705 | return
706 | }
707 | }
708 | this._refreshBackAnimationFrame = this.requestAnimationFrame(this._resetRefreshScrollTop)
709 | }
710 |
711 | _resetReverseHeaderLayout = (timestamp) => {
712 | let {pullDownStayDistance} = this.props
713 | let headerHeight
714 | if (!this._beginTimeStamp) {
715 | headerHeight = pullDownStayDistance
716 | this._beginTimeStamp = timestamp
717 | }
718 | else {
719 | headerHeight = pullDownStayDistance * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
720 | if (headerHeight > pullDownStayDistance) {
721 | headerHeight = pullDownStayDistance
722 | }
723 | }
724 | this._header.setNativeProps({
725 | style: {
726 | height: headerHeight,
727 | }
728 | })
729 | this._headerHeight = headerHeight
730 |
731 | if (timestamp - this._beginTimeStamp > refreshAnimationDuration) {
732 | this._header.setNativeProps({
733 | style: {
734 | height: pullDownStayDistance,
735 | }
736 | })
737 | this._headerHeight = pullDownStayDistance
738 |
739 | this._beginTimeStamp = null
740 | this._refreshBackAnimating = false
741 | this._afterRefreshBacked = true
742 |
743 | this.props.onRefresh && this.props.onRefresh()
744 |
745 | return
746 | }
747 |
748 | this.requestAnimationFrame(this._resetReverseHeaderLayout)
749 | }
750 |
751 | _resetHeaderLayout = (timestamp) => {
752 | let {loaded_all, load_more_none} = viewState
753 | let {pullDownStayDistance} = this.props
754 | let headerHeight
755 | if (!this._beginTimeStamp) {
756 | headerHeight = pullDownStayDistance
757 | this._beginTimeStamp = timestamp
758 | this._fixedScrollY = this._scrollY > 0 ? this._scrollY : 0
759 |
760 | let opacity = 0
761 | this._footer.setNativeProps({
762 | style: {
763 | opacity,
764 | }
765 | })
766 | }
767 | else {
768 | //headerHeight = pullDownStayDistance - (pullDownStayDistance - this._fixedScrollY) * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
769 | headerHeight = pullDownStayDistance - (pullDownStayDistance - this._fixedScrollY - this._fixedBoundary - StyleSheet.hairlineWidth) * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
770 | //if (headerHeight < 0) {
771 | // headerHeight = 0
772 | //}
773 | /**
774 | * fix the bug that onContentSizeChange sometimes is not triggered, it causes incorrect contentHeight(this._scrollViewContentHeight)
775 | */
776 | if (headerHeight < this._fixedBoundary + StyleSheet.hairlineWidth) {
777 | headerHeight = this._fixedBoundary + StyleSheet.hairlineWidth
778 | }
779 | }
780 | this._header.setNativeProps({
781 | style: {
782 | height: headerHeight,
783 | }
784 | })
785 | this._headerHeight = headerHeight
786 |
787 | if (timestamp - this._beginTimeStamp > refreshAnimationDuration) {
788 | this._header.setNativeProps({
789 | style: {
790 | //height: 0,
791 | /**
792 | * (occurs on react-native 0.32, and maybe also occurs on react-native 0.30+)ListView renderHeader/renderFooter => View's children cannot be visible when parent's height < StyleSheet.hairlineWidth
793 | * ScrollView does not exist this strange bug
794 | */
795 | height: this._fixedBoundary
796 | }
797 | })
798 | this._headerHeight = this._fixedBoundary
799 |
800 | //console.log(`this._fixedScrollY = ${this._fixedScrollY}, this._scrollY = ${this._scrollY}`)
801 | if (this._fixedScrollY > 0) {
802 | this._scrollView.scrollTo({ y: 0, animated: false, })
803 | }
804 | this._beginTimeStamp = null
805 | this._refreshBackAnimating = false
806 | this._afterRefreshBacked = true
807 |
808 | //force show footer
809 | this._footer.setNativeProps({
810 | style: {
811 | opacity: 1,
812 | }
813 | })
814 |
815 | this._setPaddingBlank()
816 |
817 | //reset loadMoreState to load_more_none
818 | if (this._loadMoreState == loaded_all) {
819 | this._loadMoreState = load_more_none
820 | this._footer.setState({
821 | pullState: this._loadMoreState,
822 | pullDistancePercent: 0,
823 | })
824 | }
825 |
826 | this._scrollView.setNativeProps({
827 | scrollEnabled: true
828 | })
829 |
830 | return
831 | }
832 |
833 | this.requestAnimationFrame(this._resetHeaderLayout)
834 | }
835 |
836 | _resetFooterLayout = (timestamp) => {
837 | let {pullUpStayDistance} = this.props
838 | let footerHeight, scrollViewTranslateY
839 | if (!this._beginTimeStamp) {
840 | footerHeight = pullUpStayDistance
841 | scrollViewTranslateY = 0
842 | this._beginTimeStamp = timestamp
843 | this._fixedScrollY = this._scrollY
844 | }
845 | else {
846 | let scrollViewTranslateMaxY
847 | //footerHeight = pullUpStayDistance - pullUpStayDistance * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
848 | footerHeight = pullUpStayDistance - (pullUpStayDistance - this._fixedBoundary - StyleSheet.hairlineWidth) * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
849 |
850 | if (this._touching && (this._fixedScrollY - (this._scrollViewContentHeight - this._scrollViewContainerHeight)) > pullUpStayDistance) {
851 | scrollViewTranslateMaxY = pullUpStayDistance
852 | }
853 | else {
854 | scrollViewTranslateMaxY = this._fixedScrollY - (this._scrollViewContentHeight - this._scrollViewContainerHeight)
855 | }
856 | scrollViewTranslateY = (scrollViewTranslateMaxY - this._fixedBoundary - StyleSheet.hairlineWidth) * (timestamp - this._beginTimeStamp) / refreshAnimationDuration
857 | //if (footerHeight < 0) {
858 | // footerHeight = 0
859 | //}
860 | //if (scrollViewTranslateY > scrollViewTranslateMaxY) {
861 | // scrollViewTranslateY = scrollViewTranslateMaxY
862 | //}
863 | /**
864 | * fix the bug that onContentSizeChange sometimes is not triggered, it causes incorrect contentHeight(this._scrollViewContentHeight)
865 | */
866 | if (footerHeight < this._fixedBoundary + StyleSheet.hairlineWidth) {
867 | footerHeight = this._fixedBoundary + StyleSheet.hairlineWidth
868 | }
869 | if (scrollViewTranslateY > scrollViewTranslateMaxY - this._fixedBoundary - StyleSheet.hairlineWidth) {
870 | scrollViewTranslateY = scrollViewTranslateMaxY - this._fixedBoundary - StyleSheet.hairlineWidth
871 | }
872 | }
873 |
874 | this._footer.setNativeProps({
875 | style: {
876 | height: footerHeight,
877 | }
878 | })
879 | this._scrollView.scrollTo({ y: this._fixedScrollY - scrollViewTranslateY, animated: false, })
880 |
881 | if (timestamp - this._beginTimeStamp > refreshAnimationDuration) {
882 | /**
883 | * (occurs on react-native 0.32, and maybe also occurs on react-native 0.30+)ListView renderHeader/renderFooter => View's children cannot be visible when parent's height < StyleSheet.hairlineWidth
884 | * ScrollView does not exist this strange bug
885 | */
886 | this._footer.setNativeProps({
887 | style: {
888 | height: this._fixedBoundary,
889 | }
890 | })
891 | this._scrollView.scrollTo({
892 | y: this._fixedScrollY - scrollViewTranslateY + StyleSheet.hairlineWidth,
893 | animated: false,
894 | })
895 |
896 | this._beginTimeStamp = null
897 | this._loadMoreBackAnimating = false
898 | this._afterLoadMoreBacked = true
899 |
900 | this._setPaddingBlank()
901 |
902 | this._scrollView.setNativeProps({
903 | scrollEnabled: true,
904 | })
905 |
906 | return
907 | }
908 |
909 | this.requestAnimationFrame(this._resetFooterLayout)
910 | }
911 |
912 | _renderHeader = () => {
913 | return (
914 | this._header = component }
915 | style={[styles.header, styles.shrink,]}
916 | viewType={refreshViewType.header}
917 | renderRefreshContent={this.props.renderHeader}/>
918 | )
919 | }
920 |
921 | _renderFooter = () => {
922 | return (
923 | this._footer = component }
924 | onLayout={this._onFooterLayout}
925 | style={[styles.footer, this.props.autoLoadMore ? null : styles.shrink, { opacity: 0, }, ]}
926 | viewType={refreshViewType.footer}
927 | renderRefreshContent={this.props.renderFooter}/>
928 | )
929 | }
930 |
931 | _onFooterLayout = (e) => {
932 | this._autoLoadFooterHeight = e.nativeEvent.layout.height
933 | }
934 |
935 | //only used by listview
936 | _renderRow = (rowData, sectionID, rowID) => {
937 | let {listItemProps, renderRow,} = this.props
938 | if (listItemProps) {
939 | return (
940 | this._listItemRefs[rowID] = component} {...listItemProps}>
941 | {renderRow(rowData, sectionID, rowID)}
942 |
943 | )
944 | }
945 | else {
946 | return renderRow(rowData, sectionID, rowID)
947 | }
948 | }
949 |
950 | //only used by listview
951 | _onChangeVisibleRows = (visibleRows, changedRows) => {
952 | let {listItemProps, onChangeVisibleRows,} = this.props
953 | if (listItemProps) {
954 | Object.keys(changedRows).forEach(sectionID => {
955 | let section = changedRows[ sectionID ]
956 | Object.keys(section).forEach(rowID => {
957 | let listItemRef = this._listItemRefs[ rowID ];
958 | if (section[ rowID ]) {
959 | //console.log(`show rowID = ${rowID}`)
960 | listItemRef.show()
961 | }
962 | else {
963 | //console.log(`hide rowID = ${rowID}`)
964 | listItemRef.hide()
965 | }
966 | })
967 | })
968 | }
969 | onChangeVisibleRows && onChangeVisibleRows(visibleRows, changedRows)
970 | }
971 |
972 | clearListRowRefsCache = () => {
973 | this._listItemRefs = {}
974 | }
975 | }
976 |
977 | export default TimerEnhance(PullToRefreshListView)
978 |
--------------------------------------------------------------------------------
/PullToRefreshListView.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A smart pull-down-refresh and pull-up-loadmore react-native listview
3 | * https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview/
4 | * Released under the MIT license
5 | * Copyright (c) 2016 react-native-component
6 | */
7 |
8 |
9 | import {
10 | Platform,
11 | } from 'react-native'
12 |
13 | import AndroidPullToRefreshListView from './PullToRefreshListView-android'
14 | import IOSPullToRefreshListView from './PullToRefreshListView-ios'
15 |
16 | let PullToRefreshListView
17 |
18 | if(Platform.OS == 'ios') {
19 | PullToRefreshListView = IOSPullToRefreshListView
20 | }
21 | else {
22 | PullToRefreshListView = AndroidPullToRefreshListView
23 | }
24 |
25 | export default PullToRefreshListView
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-smart-pull-to-refresh-listview
2 |
3 | [](https://www.npmjs.com/package/react-native-smart-pull-to-refresh-listview)
4 | [](https://www.npmjs.com/package/react-native-smart-pull-to-refresh-listview)
5 | [](https://www.npmjs.com/package/react-native-smart-pull-to-refresh-listview)
6 | [](https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview/blob/master/LICENSE)
7 |
8 | A smart pull-down-refresh and pull-up-loadmore react-native listview,
9 | for ios, written in pure JS, for android, written in JS and Java.
10 |
11 | This component is compatible with React Native 0.25 and newer.
12 |
13 | ## Preview
14 |
15 | ![react-native-pull-to-refresh-listview-preview-ios][1]
16 | ![react-native-pull-to-refresh-listview-preview-android][2]
17 |
18 | ## Advanced Features
19 |
20 | * Flexible pull to refresh control for ios and android,
21 |
22 | easy to customize the 'RefreshView' style and content,
23 | bounce effect for both pull down refresh and pull up load more,
24 | if you want, you can also use the 'autoLoad' mode for pull up load more.
25 | [demonstration][101]
26 |
27 | * Memory management for ios and android,
28 |
29 | if you want, the listRow can remove its children to release memory when its position is outside viewport of device,
30 | and will undo when its position is inside viewport of device.
31 | [demonstration][102]
32 |
33 | * Extended support sticky header for android
34 |
35 | it also supports sticky header with pull to refresh
36 | [demonstration][103]
37 |
38 |
39 | ## Installation
40 |
41 | ```
42 | npm install react-native-smart-pull-to-refresh-listview --save
43 | ```
44 |
45 | ## Installation (Android)
46 |
47 | * In `android/settings.gradle`
48 |
49 | ```
50 | ...
51 | include ':react-native-smart-swipe-refresh-layout'
52 | project(':react-native-smart-swipe-refresh-layout').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-smart-pull-to-refresh-listview/android')
53 | ```
54 |
55 | * In `android/app/build.gradle`
56 |
57 | ```
58 | ...
59 | dependencies {
60 | ...
61 | // From node_modules
62 | compile project(':react-native-smart-swipe-refresh-layout')
63 | }
64 | ```
65 |
66 | * In MainApplication.java
67 |
68 | ```
69 | ...
70 | import com.reactnativecomponent.swiperefreshlayout.RCTSwipeRefreshLayoutPackage; //import package
71 | ...
72 | /**
73 | * A list of packages used by the app. If the app uses additional views
74 | * or modules besides the default ones, add more packages here.
75 | */
76 | @Override
77 | protected List getPackages() {
78 | return Arrays.asList(
79 | new MainReactPackage(),
80 | new RCTSwipeRefreshLayoutPackage() //register Module
81 | );
82 | }
83 | ...
84 |
85 | ```
86 |
87 | * If you're using react-native 0.30-, follow these extra steps
88 |
89 | * In node_modules/react-native-smart-pull-to-refresh-listview/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/
90 |
91 | * In TouchEvent.java
92 |
93 | ```
94 | ...
95 | public TouchEvent(int viewTag, long timestampMs, int movement) {
96 | super(viewTag, timestampMs); //for older version
97 | //super(viewTag); //for newer version
98 | this.movement = movement;
99 | }
100 | ...
101 | ```
102 | * In TouchUpEvent.java
103 |
104 | ```
105 | ...
106 | public TouchUpEvent(int viewTag, long timestampMs) {
107 | super(viewTag, timestampMs); //for older verion
108 | //super(viewTag); //for newer version
109 | }
110 | ...
111 | ```
112 |
113 | ## Full Demo
114 |
115 | see [ReactNativeComponentDemos][0]
116 |
117 | ## Usage
118 |
119 | ```js
120 | import React, {
121 | Component,
122 | } from 'react'
123 | import {
124 | View,
125 | Text,
126 | StyleSheet,
127 | Alert,
128 | ScrollView,
129 | ListView,
130 | Image,
131 | ActivityIndicator,
132 | ProgressBarAndroid,
133 | ActivityIndicatorIOS,
134 | Platform,
135 | } from 'react-native'
136 |
137 | import TimerEnhance from 'react-native-smart-timer-enhance'
138 | import PullToRefreshListView from 'react-native-smart-pull-to-refresh-listview'
139 |
140 | export default class PullToRefreshListViewDemo extends Component {
141 |
142 | // 构造
143 | constructor(props) {
144 | super(props);
145 |
146 | this._dataSource = new ListView.DataSource({
147 | rowHasChanged: (r1, r2) => r1 !== r2,
148 | //sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
149 | });
150 |
151 | let dataList = []
152 |
153 | this.state = {
154 | first: true,
155 | dataList: dataList,
156 | dataSource: this._dataSource.cloneWithRows(dataList),
157 | }
158 | }
159 |
160 | componentDidMount () {
161 | this._pullToRefreshListView.beginRefresh()
162 | }
163 |
164 | //Using ListView
165 | render() {
166 | return (
167 | this._pullToRefreshListView = component }
169 | viewType={PullToRefreshListView.constants.viewType.listView}
170 | contentContainerStyle={{backgroundColor: 'yellow', }}
171 | style={{marginTop: Platform.OS == 'ios' ? 64 : 56, }}
172 | initialListSize={20}
173 | enableEmptySections={true}
174 | dataSource={this.state.dataSource}
175 | pageSize={20}
176 | renderRow={this._renderRow}
177 | renderHeader={this._renderHeader}
178 | renderFooter={this._renderFooter}
179 | //renderSeparator={(sectionID, rowID) => }
180 | onRefresh={this._onRefresh}
181 | onLoadMore={this._onLoadMore}
182 | pullUpDistance={35}
183 | pullUpStayDistance={50}
184 | pullDownDistance={35}
185 | pullDownStayDistance={50}
186 | />
187 | )
188 |
189 | }
190 |
191 | _renderRow = (rowData, sectionID, rowID) => {
192 | return (
193 |
194 |
195 | {rowData.text}
196 |
197 |
198 | )
199 | }
200 |
201 | _renderHeader = (viewState) => {
202 | let {pullState, pullDistancePercent} = viewState
203 | let {refresh_none, refresh_idle, will_refresh, refreshing,} = PullToRefreshListView.constants.viewState
204 | pullDistancePercent = Math.round(pullDistancePercent * 100)
205 | switch(pullState) {
206 | case refresh_none:
207 | return (
208 |
209 | pull down to refresh
210 |
211 | )
212 | case refresh_idle:
213 | return (
214 |
215 | pull down to refresh{pullDistancePercent}%
216 |
217 | )
218 | case will_refresh:
219 | return (
220 |
221 | release to refresh{pullDistancePercent > 100 ? 100 : pullDistancePercent}%
222 |
223 | )
224 | case refreshing:
225 | return (
226 |
227 | {this._renderActivityIndicator()}refreshing
228 |
229 | )
230 | }
231 | }
232 |
233 | _renderFooter = (viewState) => {
234 | let {pullState, pullDistancePercent} = viewState
235 | let {load_more_none, load_more_idle, will_load_more, loading_more, loaded_all, } = PullToRefreshListView.constants.viewState
236 | pullDistancePercent = Math.round(pullDistancePercent * 100)
237 | switch(pullState) {
238 | case load_more_none:
239 | return (
240 |
241 | pull up to load more
242 |
243 | )
244 | case load_more_idle:
245 | return (
246 |
247 | pull up to load more{pullDistancePercent}%
248 |
249 | )
250 | case will_load_more:
251 | return (
252 |
253 | release to load more{pullDistancePercent > 100 ? 100 : pullDistancePercent}%
254 |
255 | )
256 | case loading_more:
257 | return (
258 |
259 | {this._renderActivityIndicator()}loading
260 |
261 | )
262 | case loaded_all:
263 | return (
264 |
265 | no more
266 |
267 | )
268 | }
269 | }
270 |
271 | _onRefresh = () => {
272 | //console.log('outside _onRefresh start...')
273 |
274 | //simulate request data
275 | this.setTimeout( () => {
276 |
277 | //console.log('outside _onRefresh end...')
278 | let addNum = 20
279 | let refreshedDataList = []
280 | for(let i = 0; i < addNum; i++) {
281 | refreshedDataList.push({
282 | text: `item-${i}`
283 | })
284 | }
285 |
286 | this.setState({
287 | dataList: refreshedDataList,
288 | dataSource: this._dataSource.cloneWithRows(refreshedDataList),
289 | })
290 | this._pullToRefreshListView.endRefresh()
291 |
292 | }, 3000)
293 | }
294 |
295 | _onLoadMore = () => {
296 | //console.log('outside _onLoadMore start...')
297 |
298 | this.setTimeout( () => {
299 |
300 | //console.log('outside _onLoadMore end...')
301 |
302 | let length = this.state.dataList.length
303 | let addNum = 20
304 | let addedDataList = []
305 | if(length >= 100) {
306 | addNum = 3
307 | }
308 | for(let i = length; i < length + addNum; i++) {
309 | addedDataList.push({
310 | text: `item-${i}`
311 | })
312 | }
313 | let newDataList = this.state.dataList.concat(addedDataList)
314 | this.setState({
315 | dataList: newDataList,
316 | dataSource: this._dataSource.cloneWithRows(newDataList),
317 | })
318 |
319 | let loadedAll
320 | if(length >= 100) {
321 | loadedAll = true
322 | this._pullToRefreshListView.endLoadMore(loadedAll)
323 | }
324 | else {
325 | loadedAll = false
326 | this._pullToRefreshListView.endLoadMore(loadedAll)
327 | }
328 |
329 | }, 3000)
330 | }
331 |
332 | _renderActivityIndicator() {
333 | return ActivityIndicator ? (
334 |
339 | ) : Platform.OS == 'android' ?
340 | (
341 |
345 |
346 | ) : (
347 |
352 | )
353 | }
354 |
355 | }
356 |
357 |
358 |
359 | const styles = StyleSheet.create({
360 | itemHeader: {
361 | height: 35,
362 | borderBottomWidth: StyleSheet.hairlineWidth,
363 | borderBottomColor: '#ccc',
364 | backgroundColor: 'blue',
365 | overflow: 'hidden',
366 | justifyContent: 'center',
367 | alignItems: 'center',
368 | },
369 | item: {
370 | height: 60,
371 | //borderBottomWidth: StyleSheet.hairlineWidth,
372 | //borderBottomColor: '#ccc',
373 | overflow: 'hidden',
374 | justifyContent: 'center',
375 | alignItems: 'center',
376 | },
377 |
378 | contentContainer: {
379 | paddingTop: 20 + 44,
380 | },
381 |
382 | thumbnail: {
383 | padding: 6,
384 | flexDirection: 'row',
385 | borderBottomWidth: StyleSheet.hairlineWidth,
386 | borderBottomColor: '#ccc',
387 | overflow: 'hidden',
388 | },
389 |
390 | textContainer: {
391 | padding: 20,
392 | flex: 1,
393 | justifyContent: 'center',
394 | alignItems: 'center',
395 | }
396 | })
397 |
398 | export default TimerEnhance(PullToRefreshListViewDemo)
399 | ```
400 |
401 | ## Props
402 |
403 | Prop | Type | Optional | Default | Description
404 | ----------------------- | ------ | -------- | --------- | -----------
405 | ...ListView.propTypes | | | | see [react-native documents][3]
406 | viewType | enum | Yes | Symbol | determines the viewType which will be used(ScrollView, ListView)
407 | autoLoadMore | bool | Yes | false | when the value is true, pull up load more will be auto
408 | onRefresh | func | Yes | | when refreshing, this function will be called
409 | onLoadMore | func | Yes | | when loadingMore, this function will be called
410 | onEndReachedThreshold | number | Yes | 0 | threshold in pixels (virtual, not physical) for calling onLoadMore
411 | pullUpDistance | number | Yes | 35 | determines the pull up max distance
412 | pullUpStayDistance | number | Yes | 50 | determines the pull up stay distance
413 | pullDownDistance | number | Yes | 35 | determines the pull down max distance
414 | pullDownStayDistance | number | Yes | 50 | determines the pull down stay distance
415 | enabledPullUp | bool | Yes | true | when the value is false, pull up load more will be disabled
416 | enabledPullDown | bool | Yes | true | when the value is false, pull down refresh will be disabled
417 | listItemProps | object | Yes | | see [react-native documents][4]
418 | renderRowWithVisibility | bool | Yes | | when the value is true, the children of the listRow can be controlled with 'hidden' state
419 | pageTop | number | Yes | 0 | determines the top relative to the page of the float section header(sticky header) view
420 | floatSectionHeaderWidth | number | Yes |deviceWidth| determines the width of the float section header(sticky header) view
421 | renderFloatSectionHeader| number | Yes | 0 | determines the width of the float section header(sticky header) view
422 | listSectionProps | object | Yes | | see [react-native documents][4]
423 |
424 | ## Special Props
425 |
426 | * listItemProps: when set this prop, listView will use special 'listRow',
427 | the listRow will remove its children to release memory when its position is outside viewport of device,
428 | and will undo when its position is inside viewport of device.
429 | Usually it is used with 'react-native-smart-image-loader'
430 |
431 | * renderRowWithVisibility: when the value is true,
432 | the children of the listRow can be controlled with 'hidden' state.
433 | This prop is valid when 'listItemProps' is being set, and it is only valid for android.
434 | Usually it is used with 'react-native-smart-image-loader'
435 |
436 | * pageTop, floatSectionHeaderWidth, renderFloatSectionHeader, listSectionProps
437 | are used for android to support ios-like sticky header
438 |
439 | ## Method
440 |
441 | * beginRefresh(bounceDisabled): force begin pull down refresh, if bounceDisabled is true, the bounce animation will be disabled
442 | * endRefresh(bounceDisabled): end pull down refresh, if bounceDisabled is true, the bounce animation will be disabled
443 | * endLoadMore: end pull up load more
444 |
445 |
446 | [0]: https://github.com/cyqresig/ReactNativeComponentDemos
447 | [1]: http://cyqresig.github.io/img/react-native-smart-pull-to-refresh-preview-v1.6.0.gif
448 | [2]: http://cyqresig.github.io/img/react-native-smart-pull-to-refresh-preview-android-v1.6.0.gif
449 | [3]: http://facebook.github.io/react-native/docs/listview.html#props
450 | [4]: http://facebook.github.io/react-native/docs/view.html#props
451 |
452 | [101]: http://cyqresig.github.io/img/pull-to-refresh-flexible-pull-to-refresh-control.gif
453 | [102]: http://cyqresig.github.io/img/pull-to-refresh-memory-management.gif
454 | [103]: http://cyqresig.github.io/img/pull-to-refresh-extended-support-sticky-header-for-android.gif
--------------------------------------------------------------------------------
/RefreshView.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | } from 'react'
4 | import PropTypes from 'prop-types';
5 | import {
6 | View,
7 | ViewPropTypes,
8 | } from 'react-native'
9 |
10 | import constants from './constants'
11 |
12 | export default class RefreshView extends Component {
13 |
14 | static defaultProps = {
15 | renderRefreshContent: () => null,
16 | }
17 |
18 | static propTypes = {
19 | ...ViewPropTypes,
20 | renderRefreshContent: PropTypes.func,
21 | }
22 |
23 | constructor (props) {
24 | super(props)
25 | this.state = {
26 | pullState: props.viewType == constants.refreshViewType.header ? constants.viewState.refresh_none : constants.viewState.load_more_none,
27 | pullDistancePercent: 0,
28 | }
29 | }
30 |
31 | setNativeProps (props) {
32 | this._refreshView.setNativeProps(props)
33 | }
34 |
35 | render () {
36 | return (
37 | this._refreshView = component } {...this.props}>
38 | {this.props.renderRefreshContent(this.state)}
39 |
40 | )
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 23
5 | buildToolsVersion "23.0.3"
6 |
7 | defaultConfig {
8 | minSdkVersion 16
9 | targetSdkVersion 23
10 | versionCode 1
11 | versionName "1.0"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | }
20 |
21 | dependencies {
22 | compile fileTree(dir: 'libs', include: ['*.jar'])
23 | compile 'com.android.support:appcompat-v7:23.4.0'
24 | compile 'com.facebook.react:react-native:+'
25 | }
26 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/OnEvChangeListener.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 | /**
4 | * Created by shiyunjie on 16/11/2.
5 | */
6 |
7 | public interface OnEvChangeListener {
8 | void onWindowVisibilityChange(boolean hiddenState);
9 | }
10 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/RCTLazyLoadView.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 | import android.content.Context;
4 | import android.graphics.Point;
5 | import android.graphics.Rect;
6 | import android.util.Log;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 |
10 |
11 | public class RCTLazyLoadView extends ViewGroup {
12 |
13 | private OnEvChangeListener onEvChangeListener;
14 |
15 |
16 | public RCTLazyLoadView(Context context) {
17 | super(context);
18 | }
19 |
20 | @Override
21 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
22 |
23 | }
24 |
25 | // @Override
26 | // protected void dispatchVisibilityChanged(View changedView, int visibility) {
27 | // super.dispatchVisibilityChanged(changedView, visibility);
28 | // Log.i("1Test", "dispatchVisibilityChanged");
29 | // }
30 | //
31 | // @Override
32 | // public void dispatchWindowVisibilityChanged(int visibility) {
33 | // super.dispatchWindowVisibilityChanged(visibility);
34 | // Log.i("1Test", "dispatchWindowVisibilityChanged");
35 | // }
36 |
37 |
38 | // @Override
39 | // protected void onVisibilityChanged(View changedView, int visibility) {
40 | // super.onVisibilityChanged(changedView, visibility);
41 | // Log.i("1Test", "onVisibilityChanged");
42 | // }
43 | //
44 | // @Override
45 | // public boolean getChildVisibleRect(View child, Rect r, Point offset) {
46 | // Log.i("1Test", "getChildVisibleRect");
47 | // return super.getChildVisibleRect(child, r, offset);
48 | // }
49 |
50 | @Override
51 | protected void onWindowVisibilityChanged(int visibility) {
52 | // Log.i("1Test", "onWindowVisibilityChanged");
53 | super.onWindowVisibilityChanged(visibility);
54 | Boolean hiddenState = false;
55 | if (visibility == View.VISIBLE) {
56 | // Log.i("1Test", "View可见=========true");
57 | hiddenState = false;
58 | } else if (visibility == View.INVISIBLE || visibility == View.GONE) {
59 | // Log.i("1Test", "View隐藏=========false");
60 | hiddenState = true;
61 | }
62 | onEvChangeListener.onWindowVisibilityChange(hiddenState);
63 | }
64 |
65 | public void setOnEvChangeListener(OnEvChangeListener onEvChangeListener) {
66 | // Log.i("1Test", "setOnEvChangeListener");
67 | this.onEvChangeListener = onEvChangeListener;
68 | }
69 |
70 | // protected boolean isCover() {
71 | // boolean cover = false;
72 | // Rect rect = new Rect();
73 | // cover = getGlobalVisibleRect(rect);
74 | //// Log.i("1Test","可见区域:"+rect.width()+", 高度:"+rect.height());
75 | //
76 | // return true;
77 | // }
78 | }
79 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/RCTLazyLoadViewManager.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 |
4 | import com.facebook.react.common.MapBuilder;
5 | import com.facebook.react.common.SystemClock;
6 | import com.facebook.react.uimanager.ThemedReactContext;
7 | import com.facebook.react.uimanager.UIManagerModule;
8 | import com.facebook.react.uimanager.ViewGroupManager;
9 |
10 | import java.util.Map;
11 |
12 | /**
13 | * Created by shiyunjie on 16/8/4.
14 | */
15 | public class RCTLazyLoadViewManager extends ViewGroupManager {
16 | private static final String REACT_CLASS = "RCTLazyLoadView";//要与类名一致
17 |
18 |
19 | @Override
20 | public String getName() {
21 | return REACT_CLASS;
22 | }
23 |
24 | @Override
25 | public RCTLazyLoadView createViewInstance(final ThemedReactContext reactContext) {
26 |
27 | return new RCTLazyLoadView(reactContext);
28 | }
29 |
30 |
31 |
32 | @Override
33 | protected void addEventEmitters(
34 | final ThemedReactContext reactContext,
35 | final RCTLazyLoadView view) {
36 | view.setOnEvChangeListener(
37 | new OnEvChangeListener() {
38 | @Override
39 | public void onWindowVisibilityChange( boolean hiddenState) {
40 | reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
41 | .dispatchEvent(new WindowVisibilityChangeEvent(view.getId(), SystemClock.nanoTime(), hiddenState));
42 | }
43 |
44 | });
45 | }
46 |
47 | @Override
48 | public Map getExportedCustomDirectEventTypeConstants() {
49 | return MapBuilder.builder()
50 | .put("RCTLayzyLoadView.onWindowVisibilityChange", MapBuilder.of("registrationName", "onWindowVisibilityChange"))//registrationName 后的名字,RN中方法也要是这个名字否则不执行
51 | .build();
52 | }
53 |
54 |
55 |
56 | }
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/RCTSwipeRefreshLayout.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.support.v4.view.MotionEventCompat;
6 | import android.support.v4.view.NestedScrollingChild;
7 | import android.support.v4.view.NestedScrollingChildHelper;
8 | import android.support.v4.view.NestedScrollingParent;
9 | import android.support.v4.view.NestedScrollingParentHelper;
10 | import android.support.v4.view.ViewCompat;
11 | import android.support.v4.widget.SwipeRefreshLayout;
12 | import android.util.AttributeSet;
13 | import android.util.DisplayMetrics;
14 | import android.util.Log;
15 | import android.view.MotionEvent;
16 | import android.view.View;
17 | import android.view.ViewConfiguration;
18 | import android.view.ViewGroup;
19 | import android.widget.AbsListView;
20 | import android.widget.ScrollView;
21 |
22 | import com.facebook.react.uimanager.events.NativeGestureUtil;
23 |
24 |
25 | public class RCTSwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,
26 | NestedScrollingChild {
27 |
28 |
29 | private static final String LOG_TAG = RCTSwipeRefreshLayout.class.getSimpleName();
30 |
31 | private static final int INVALID_POINTER = -1;
32 | private static final float DRAG_RATE = .5f;
33 |
34 | private View mTarget; // the target of the gesture
35 | private OnEvTouchListener mTouchListener;
36 | private boolean mRefreshing = false;
37 | private boolean enabledPullUp = true;
38 | private boolean enabledPullDown = true;
39 | private int mTouchSlop;
40 | private float mPrevTouchX;
41 | private boolean mIntercepted;
42 |
43 |
44 | // If nested scrolling is enabled, the total amount that needed to be
45 | // consumed by this as the nested scrolling parent is used in place of the
46 | // overscroll determined by MOVE events in the onTouch handler
47 | private float mTotalUnconsumed;
48 | private final NestedScrollingParentHelper mNestedScrollingParentHelper;
49 | private final NestedScrollingChildHelper mNestedScrollingChildHelper;
50 | private final int[] mParentScrollConsumed = new int[2];
51 | private final int[] mParentOffsetInWindow = new int[2];
52 |
53 | // private float mInitialMotionY;
54 | private float mInitialDownY;
55 | private boolean mIsBeingDragged;//拖动中
56 | private int mActivePointerId = INVALID_POINTER;
57 |
58 | // Target is returning to its start offset because it was cancelled or a
59 | // refresh was triggered.
60 | private boolean mReturningToStart;
61 | private static final int[] LAYOUT_ATTRS = new int[]{
62 | android.R.attr.enabled
63 | };
64 |
65 |
66 | private float mLastMargin;
67 | private float mMoveMargin;
68 |
69 |
70 | private float density;
71 |
72 | private int moveDirection = 0; //1表示下拉, -1表示上拉
73 |
74 | // int scrollViewMeasuredHeight;
75 |
76 | /**
77 | * Simple constructor to use when creating a SwipeRefreshLayout from code.
78 | *
79 | * @param context
80 | */
81 | public RCTSwipeRefreshLayout(Context context) {
82 | this(context, null);
83 | DisplayMetrics dm = new DisplayMetrics();
84 | density = context.getResources().getDisplayMetrics().density;
85 |
86 | ViewCompat.setChildrenDrawingOrderEnabled(this, true);
87 | // the absolute offset has to take into account that the circle starts at an offset
88 | final DisplayMetrics metrics = getResources().getDisplayMetrics();
89 |
90 |
91 | setNestedScrollingEnabled(true);
92 | }
93 |
94 | /**
95 | * Constructor that is called when inflating SwipeRefreshLayout from XML.
96 | *
97 | * @param context
98 | * @param attrs
99 | */
100 | public RCTSwipeRefreshLayout(Context context, AttributeSet attrs) {
101 | super(context, attrs);
102 |
103 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
104 |
105 | setWillNotDraw(false);
106 |
107 |
108 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
109 | setEnabled(a.getBoolean(0, true));
110 | a.recycle();
111 |
112 | density = context.getResources().getDisplayMetrics().density;
113 |
114 | ViewCompat.setChildrenDrawingOrderEnabled(this, true);
115 |
116 | mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
117 |
118 | mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
119 | setNestedScrollingEnabled(true);
120 | }
121 |
122 |
123 | public void setOnEvTouchListener(OnEvTouchListener mTouchListener) {
124 | this.mTouchListener = mTouchListener;
125 | }
126 |
127 |
128 | private void ensureTarget() {
129 | // Don't bother getting the parent height if the parent hasn't been laid
130 | // out yet.
131 | if (mTarget == null) {
132 | for (int i = 0; i < getChildCount(); i++) {
133 | View child = getChildAt(0);
134 | mTarget = child;
135 | break;
136 | }
137 | }
138 | }
139 |
140 | @Override
141 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
142 | // final ScrollView scrollView = (ScrollView) mTarget;
143 | // scrollViewMeasuredHeight = scrollView.getChildAt(0).getMeasuredHeight();
144 | // scrollViewMeasuredHeight = scrollView.getMeasuredHeight();
145 | // Log.i("Test", "onLayout scrollViewMeasuredHeight = " + scrollViewMeasuredHeight);
146 | }
147 |
148 | @Override
149 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
150 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
151 | if (mTarget == null) {
152 | ensureTarget();
153 | }
154 | if (mTarget == null) {
155 | return;
156 | }
157 | mTarget.measure(MeasureSpec.makeMeasureSpec(
158 | getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
159 | MeasureSpec.EXACTLY),
160 | MeasureSpec.makeMeasureSpec(
161 | getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
162 | }
163 |
164 | /**
165 | * @return Whether it is possible for the child view of this layout to
166 | * scroll up. Override this if the child view is a custom view.
167 | */
168 | public boolean canChildScrollUp(float f) {
169 | boolean flag = false;
170 |
171 | if (!mRefreshing) {
172 | if (mTarget instanceof ScrollView) {
173 |
174 | final ScrollView scrollView = (ScrollView) mTarget;
175 | int scrollY = scrollView.getScrollY();
176 | int height = scrollView.getHeight();
177 | int scrollViewMeasuredHeight = scrollView.getChildAt(0).getMeasuredHeight();
178 |
179 | // Log.i("Test", "canChildScrollUp scrollY = " + scrollY + "| height = " + height + " | scrollViewMeasuredHeight = " + scrollViewMeasuredHeight);
180 |
181 | if (enabledPullDown && scrollY == 0 && f > 0) {
182 | //滑动到了顶端 view.getScrollY()="+scrollY);
183 | //Log.i("Test", "canChildScrollUp:" + f);
184 | flag = false;
185 | } else if (enabledPullUp && (scrollY + height) >= scrollViewMeasuredHeight && f < 0) {
186 | //滑动到了底部 scrollY="+scrollY);
187 | //Log.i("Test", "canChildScrollUp:" + f);
188 | flag = false;
189 | } else {
190 | flag = true;
191 | }
192 | }
193 | } else {
194 | flag = true;
195 | }
196 | return flag;
197 | }
198 |
199 | @Override
200 | public boolean onInterceptTouchEvent(MotionEvent ev) {
201 | ensureTarget();
202 |
203 | final int action = MotionEventCompat.getActionMasked(ev);
204 |
205 | if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
206 | mReturningToStart = false;
207 | }
208 |
209 | switch (action) {
210 | case MotionEvent.ACTION_DOWN:
211 |
212 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
213 |
214 | //Log.i("Test", "ACTION_DOWN:"+mActivePointerId);
215 |
216 | mIsBeingDragged = false;
217 | final float initialDownY = getMotionEventY(ev, mActivePointerId);
218 | if (initialDownY == -1) {
219 | return false;
220 | }
221 | mInitialDownY = initialDownY;
222 |
223 | //Log.i("Test", "ACTION_DOWN:"+initialDownY);
224 | break;
225 |
226 | case MotionEvent.ACTION_MOVE:
227 |
228 | if (mActivePointerId == INVALID_POINTER) {
229 | // Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
230 | return false;
231 | }
232 |
233 | // final float y = getMotionEventY(ev, mActivePointerId);
234 | final float y = ev.getY();
235 | if (y == -1) {
236 | return false;
237 | }
238 | final float yDiff = (y - mInitialDownY) * DRAG_RATE;
239 | if (yDiff > mTouchSlop || yDiff < -mTouchSlop) {
240 | // mInitialMotionY = mInitialDownY + mTouchSlop;
241 | mIsBeingDragged = !canChildScrollUp(yDiff);
242 |
243 | return mIsBeingDragged;
244 | }
245 | break;
246 |
247 | /* case MotionEventCompat.ACTION_POINTER_UP:
248 | onSecondaryPointerUp(ev);
249 | break;*/
250 |
251 | case MotionEvent.ACTION_UP:
252 | case MotionEvent.ACTION_CANCEL:
253 | mIsBeingDragged = false;
254 | mActivePointerId = INVALID_POINTER;
255 |
256 | break;
257 | }
258 |
259 | return mIsBeingDragged;
260 | }
261 |
262 | /**
263 | * completely bypasses ViewGroup's "disallowIntercept" by overriding
264 | * and never calling super.onInterceptTouchEvent().
265 | * This means that horizontal scrolls will always be intercepted, even though they shouldn't, so
266 | * we have to check for that manually here.
267 | */
268 | private boolean shouldInterceptTouchEvent(MotionEvent ev) {
269 | switch (ev.getAction()) {
270 | case MotionEvent.ACTION_DOWN:
271 | mPrevTouchX = ev.getX();
272 | mIntercepted = false;
273 | break;
274 |
275 | case MotionEvent.ACTION_MOVE:
276 | final float eventX = ev.getX();
277 | final float xDiff = Math.abs(eventX - mPrevTouchX);
278 |
279 | if (mIntercepted || xDiff > mTouchSlop) {
280 | mIntercepted = true;
281 | return false;
282 | }
283 | }
284 | return true;
285 | }
286 |
287 | private float getMotionEventY(MotionEvent ev, int activePointerId) {
288 | final int index = MotionEventCompat.findPointerIndex(ev, activePointerId);
289 | if (index < 0) {
290 | return -1;
291 | }
292 | return MotionEventCompat.getY(ev, index);
293 | }
294 |
295 | /**
296 | * {@link SwipeRefreshLayout} overrides {@link ViewGroup#requestDisallowInterceptTouchEvent} and
297 | * swallows it. This means that any component underneath SwipeRefreshLayout will now interact
298 | * incorrectly with Views that are above SwipeRefreshLayout. We fix that by transmitting the call
299 | * to this View's parents.
300 | */
301 | @Override
302 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
303 | if (getParent() != null) {
304 | getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
305 | }
306 | }
307 |
308 | // NestedScrollingParent
309 |
310 | @Override
311 | public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
312 | return isEnabled() && !mReturningToStart
313 | && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
314 | }
315 |
316 | @Override
317 | public void onNestedScrollAccepted(View child, View target, int axes) {
318 | // Reset the counter of how much leftover scroll needs to be consumed.
319 | mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
320 | // Dispatch up to the nested parent
321 | startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
322 | mTotalUnconsumed = 0;
323 | }
324 |
325 | @Override
326 | public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
327 | // If we are in the middle of consuming, a scroll, then we want to move the spinner back up
328 | // before allowing the list to scroll
329 | if (dy > 0 && mTotalUnconsumed > 0) {
330 | if (dy > mTotalUnconsumed) {
331 | consumed[1] = dy - (int) mTotalUnconsumed;
332 | mTotalUnconsumed = 0;
333 | } else {
334 | mTotalUnconsumed -= dy;
335 | consumed[1] = dy;
336 | }
337 |
338 | }
339 |
340 | // Now let our nested parent consume the leftovers
341 | final int[] parentConsumed = mParentScrollConsumed;
342 | if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
343 | consumed[0] += parentConsumed[0];
344 | consumed[1] += parentConsumed[1];
345 | }
346 | }
347 |
348 | @Override
349 | public int getNestedScrollAxes() {
350 | return mNestedScrollingParentHelper.getNestedScrollAxes();
351 | }
352 |
353 | @Override
354 | public void onStopNestedScroll(View target) {
355 | mNestedScrollingParentHelper.onStopNestedScroll(target);
356 | // Finish the spinner for nested scrolling if we ever consumed any
357 | // unconsumed nested scroll
358 | if (mTotalUnconsumed > 0) {
359 |
360 | mTotalUnconsumed = 0;
361 | }
362 | // Dispatch up our nested parent
363 | stopNestedScroll();
364 | }
365 |
366 | @Override
367 | public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
368 | final int dxUnconsumed, final int dyUnconsumed) {
369 | // Dispatch up to the nested parent first
370 | dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
371 | mParentOffsetInWindow);
372 |
373 | // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
374 | // sometimes between two nested scrolling views, we need a way to be able to know when any
375 | // nested scrolling parent has stopped handling events. We do that by using the
376 | // 'offset in window 'functionality to see if we have been moved from the event.
377 | // This is a decent indication of whether we should take over the event stream or not.
378 | final int dy = dyUnconsumed + mParentOffsetInWindow[1];
379 | if (dy < 0) {
380 | mTotalUnconsumed += Math.abs(dy);
381 |
382 | }
383 | }
384 |
385 | // NestedScrollingChild
386 |
387 | @Override
388 | public void setNestedScrollingEnabled(boolean enabled) {
389 | mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
390 | }
391 |
392 | @Override
393 | public boolean isNestedScrollingEnabled() {
394 | return mNestedScrollingChildHelper.isNestedScrollingEnabled();
395 | }
396 |
397 | @Override
398 | public boolean startNestedScroll(int axes) {
399 | return mNestedScrollingChildHelper.startNestedScroll(axes);
400 | }
401 |
402 | @Override
403 | public void stopNestedScroll() {
404 | mNestedScrollingChildHelper.stopNestedScroll();
405 | }
406 |
407 | @Override
408 | public boolean hasNestedScrollingParent() {
409 | return mNestedScrollingChildHelper.hasNestedScrollingParent();
410 | }
411 |
412 | @Override
413 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
414 | int dyUnconsumed, int[] offsetInWindow) {
415 | return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
416 | dxUnconsumed, dyUnconsumed, offsetInWindow);
417 | }
418 |
419 | @Override
420 | public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
421 | return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
422 | }
423 |
424 | @Override
425 | public boolean onNestedPreFling(View target, float velocityX,
426 | float velocityY) {
427 | return dispatchNestedPreFling(velocityX, velocityY);
428 | }
429 |
430 | @Override
431 | public boolean onNestedFling(View target, float velocityX, float velocityY,
432 | boolean consumed) {
433 | return dispatchNestedFling(velocityX, velocityY, consumed);
434 | }
435 |
436 | @Override
437 | public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
438 | return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
439 | }
440 |
441 | @Override
442 | public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
443 | return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
444 | }
445 |
446 | @Override
447 | public boolean onTouchEvent(MotionEvent ev) {
448 | final int action = MotionEventCompat.getActionMasked(ev);
449 | int pointerIndex = -1;
450 |
451 | if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
452 | mReturningToStart = false;
453 | }
454 |
455 | // if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
456 | // // Fail fast if we're not in a state where a swipe is possible
457 | // return false;
458 | // }
459 |
460 |
461 | switch (action) {
462 | case MotionEvent.ACTION_DOWN:
463 | // mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
464 | // mIsBeingDragged = false;
465 | // //Log.i("Test", "ACTION_DOWN:"+mActivePointerId);
466 | break;
467 |
468 | // case MotionEvent.ACTION_MOVE: {
469 | //
470 | // //判断是否scrollview是否在顶部
471 | // //
472 | // if (mTarget != null) {
473 | // pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
474 | // //Log.i("Test", "ACTION_MOVE:"+pointerIndex);
475 | // if (pointerIndex < 0) {
476 | //// Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
477 | // return false;
478 | // }
479 | // if (pointerIndex != mActivePointerId) {
480 | //
481 | // return true;
482 | // }
483 | //
484 | // final float y = MotionEventCompat.getY(ev, pointerIndex);
485 | //// final float y = ev.getY();
486 | //// //Log.i("Test", "ACTION_DOWN:" + mInitialDownY);
487 | //// //Log.i("Test", "ACTION_MOVE:" + y);
488 | // final float overscrollTop = (y - mInitialDownY) * DRAG_RATE;
489 | //
490 | // if (mMoveMargin == 0) {
491 | // mMoveMargin = overscrollTop;
492 | // } else {
493 | // mMoveMargin += overscrollTop - mLastMargin;
494 | // }
495 | //
496 | // mLastMargin = overscrollTop;
497 | // //Log.i("Test", "mLastMargin:" + mMoveMargin);
498 | // if (mMoveMargin > 0) {
499 | //
500 | // float newOverscrollTop = mMoveMargin / density;
501 | //
502 | // mTouchListener.onSwipe((int) newOverscrollTop);
503 | //// mTargetMargin = newOverscrollTop;
504 | //// moveSpinner(newOverscrollTop);
505 | // } else {
506 | //
507 | // //上拉
508 | // float newOverscrollTop = mMoveMargin / density;
509 | // mTouchListener.onSwipe((int) newOverscrollTop);
510 | //// mTargetMargin = mMoveMargin;
511 | //// moveSpinner(newOverscrollTop);
512 | // }
513 | //
514 | // }
515 | // break;
516 | // }
517 | case MotionEvent.ACTION_MOVE: {
518 | //判断是否scrollview是否在顶部
519 | //
520 | if (mTarget != null) {
521 | pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);//获得相对起始点move的点坐标
522 | if (pointerIndex < 0) {
523 | // Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
524 | return false;
525 | }
526 | if (pointerIndex != mActivePointerId) {
527 |
528 | return true;
529 | }
530 |
531 | final float y = MotionEventCompat.getY(ev, pointerIndex);
532 | // final float y = ev.getY();
533 | // //Log.i("Test", "ACTION_DOWN:" + mInitialDownY);
534 | // //Log.i("Test", "ACTION_MOVE:" + y);
535 | final float overscrollTop = (y - mInitialDownY) * DRAG_RATE;
536 |
537 |
538 | if (mMoveMargin == 0) {
539 | //之前没有赋值上一次移动距离
540 | if (moveDirection == 0) {
541 | mMoveMargin = overscrollTop;
542 | }
543 | } else if (mMoveMargin > 0) {
544 | //开始下移了 控制负数
545 | if (moveDirection == 0) {
546 | moveDirection = 1;
547 | }
548 | mMoveMargin += overscrollTop - mLastMargin;
549 |
550 | if (mLastMargin > 0 && mMoveMargin <= 0) {
551 | mMoveMargin = 0;
552 | }
553 |
554 | if (moveDirection == -1) {
555 | //出现问题
556 | mMoveMargin = 0;
557 | }
558 |
559 | } else if (mMoveMargin < 0) {
560 | //开始上移了 控制负数
561 | if (moveDirection == 0) {
562 | moveDirection = -1;
563 | }
564 | mMoveMargin += overscrollTop - mLastMargin;
565 |
566 | if (mLastMargin < 0 && mMoveMargin > 0) {
567 | mMoveMargin = 0;
568 | }
569 |
570 | if (moveDirection == 1) {
571 | //出现问题
572 | mMoveMargin = 0;
573 | }
574 |
575 | }
576 |
577 | mLastMargin = overscrollTop;
578 | //Log.i("Test", "mLastMargin:" + mMoveMargin);
579 | if (moveDirection == 1) {
580 | //下拉
581 |
582 | float newOverscrollTop = mMoveMargin / density;
583 | // Log.i("Test", "ACTION_MOVE下拉:" + newOverscrollTop);
584 | mTouchListener.onSwipe((int) newOverscrollTop);
585 | // mTargetMargin = newOverscrollTop;
586 | // moveSpinner(newOverscrollTop);
587 | } else if (moveDirection == -1) {
588 |
589 | //上拉
590 | float newOverscrollTop = mMoveMargin / density;
591 | // Log.i("Test", "ACTION_MOVE上拉:" + newOverscrollTop);
592 | mTouchListener.onSwipe((int) newOverscrollTop);
593 | // mTargetMargin = mMoveMargin;
594 | // moveSpinner(newOverscrollTop);
595 | }
596 |
597 | }
598 | break;
599 | }
600 | case MotionEventCompat.ACTION_POINTER_DOWN: {//非第一个触摸点按下
601 | //Log.i("Test", "ACTION_POINTER_DOWN");
602 | pointerIndex = MotionEventCompat.getActionIndex(ev);
603 | if (pointerIndex < 0) {
604 | // Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
605 | return false;
606 | }
607 |
608 | mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
609 | //Log.i("Test", "ACTION_POINTER_DOWN:"+mActivePointerId);
610 | final float initialDownY = getMotionEventY(ev, mActivePointerId);
611 | if (initialDownY == -1) {
612 | return false;
613 | }
614 | mInitialDownY = initialDownY;
615 | //Log.i("Test", "initialDownY:"+initialDownY);
616 | mMoveMargin += mLastMargin;
617 | break;
618 | }
619 |
620 | case MotionEventCompat.ACTION_POINTER_UP: //
621 | //Log.i("Test", "ACTION_POINTER_UP");
622 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
623 | // onSecondaryPointerUp(ev);
624 | break;
625 |
626 | case MotionEvent.ACTION_UP: {
627 |
628 | // pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
629 | //Log.i("Test", "ACTION_UP:"+pointerIndex);
630 | // if (pointerIndex < 0) {
631 | //Log.i(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
632 | // return false;
633 | // }
634 | moveDirection = 0;
635 |
636 | mLastMargin = 0;
637 | mMoveMargin = 0;
638 |
639 | mTouchListener.onSwipeRefresh();
640 |
641 | mIsBeingDragged = false;
642 | mActivePointerId = INVALID_POINTER;
643 |
644 | return false;
645 | }
646 | case MotionEvent.ACTION_CANCEL:
647 | //Log.i("Test", "ACTION_CANCEL");
648 |
649 | mIsBeingDragged = false;
650 | return false;
651 | }
652 |
653 | return true;
654 | }
655 |
656 |
657 | public void setRefreshing(boolean mRefreshing) {
658 | this.mRefreshing = mRefreshing;
659 | }
660 |
661 | public void setEnabledPullUp(boolean enabledPullUp) {
662 | this.enabledPullUp = enabledPullUp;
663 | }
664 |
665 | public void setEnalbedPullDown(boolean enalbedPullDown) {
666 | this.enabledPullDown = enalbedPullDown;
667 | }
668 |
669 |
670 | public interface OnEvTouchListener {
671 | public void onSwipe(int movement);
672 | public void onSwipeRefresh();
673 | }
674 | }
675 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/RCTSwipeRefreshLayoutManager.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 | import android.support.annotation.Nullable;
4 |
5 | import com.facebook.react.common.MapBuilder;
6 | import com.facebook.react.common.SystemClock;
7 | import com.facebook.react.uimanager.ThemedReactContext;
8 | import com.facebook.react.uimanager.UIManagerModule;
9 | import com.facebook.react.uimanager.ViewGroupManager;
10 | import com.facebook.react.uimanager.annotations.ReactProp;
11 |
12 | import java.util.Map;
13 |
14 |
15 | public class RCTSwipeRefreshLayoutManager extends ViewGroupManager {
16 |
17 | @Override
18 | public String getName() {
19 | return "RCTSwipeRefreshLayout";
20 | }
21 |
22 |
23 | @ReactProp(name = "refreshing", defaultBoolean = false)
24 | public void setRefresh(RCTSwipeRefreshLayout view, @Nullable boolean enabled) {
25 | view.setRefreshing(enabled);
26 | }
27 |
28 | @ReactProp(name = "enabledPullUp", defaultBoolean = true)
29 | public void setEnabledPullUp(RCTSwipeRefreshLayout view, @Nullable boolean enabled) {
30 | view.setEnabledPullUp(enabled);
31 | }
32 |
33 | @ReactProp(name = "enabledPullDown", defaultBoolean = true)
34 | public void setEnalbedPullDown(RCTSwipeRefreshLayout view, @Nullable boolean enabled) {
35 | view.setEnalbedPullDown(enabled);
36 | }
37 |
38 | @Override
39 | protected RCTSwipeRefreshLayout createViewInstance(ThemedReactContext reactContext) {
40 | return new RCTSwipeRefreshLayout(reactContext);
41 | }
42 |
43 |
44 | @Override
45 | protected void addEventEmitters(
46 | final ThemedReactContext reactContext,
47 | final RCTSwipeRefreshLayout view) {
48 | view.setOnEvTouchListener(
49 | new RCTSwipeRefreshLayout.OnEvTouchListener() {
50 | @Override
51 | public void onSwipe(int movement) {
52 | reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
53 | .dispatchEvent(new TouchEvent(view.getId(), SystemClock.nanoTime(), movement));
54 | }
55 |
56 | @Override
57 | public void onSwipeRefresh() {
58 | reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
59 | .dispatchEvent(new TouchUpEvent(view.getId(), SystemClock.nanoTime()));
60 | }
61 |
62 | });
63 | }
64 |
65 |
66 | @Override
67 | public Map getExportedCustomDirectEventTypeConstants() {
68 | return MapBuilder.builder()
69 | .put("RCTSwipeRefreshLayout.TouchMove", MapBuilder.of("registrationName", "onSwipe"))
70 | .put("RCTSwipeRefreshLayout.TouchUp", MapBuilder.of("registrationName", "onSwipeRefresh"))
71 | .build();
72 | }
73 |
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/RCTSwipeRefreshLayoutPackage.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 | import com.facebook.react.ReactPackage;
4 | import com.facebook.react.bridge.JavaScriptModule;
5 | import com.facebook.react.bridge.NativeModule;
6 | import com.facebook.react.bridge.ReactApplicationContext;
7 | import com.facebook.react.uimanager.ViewManager;
8 |
9 | import java.util.Arrays;
10 | import java.util.Collections;
11 | import java.util.List;
12 |
13 |
14 | public class RCTSwipeRefreshLayoutPackage implements ReactPackage {
15 |
16 |
17 | @Override
18 | public List createNativeModules(ReactApplicationContext reactContext) {
19 |
20 | return Collections.emptyList();
21 | }
22 |
23 |
24 | public List> createJSModules() {
25 | return Collections.emptyList();
26 | }
27 |
28 | @Override
29 | public List createViewManagers(ReactApplicationContext reactContext) {
30 | return Arrays.asList(new RCTSwipeRefreshLayoutManager(),
31 | new RCTLazyLoadViewManager());
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/TouchEvent.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 | import com.facebook.react.bridge.Arguments;
4 | import com.facebook.react.bridge.WritableMap;
5 | import com.facebook.react.uimanager.events.Event;
6 | import com.facebook.react.uimanager.events.RCTEventEmitter;
7 |
8 | public class TouchEvent extends Event {
9 | private int movement;
10 |
11 | public TouchEvent(int viewTag, long timestampMs, int movement) {
12 | super(viewTag);
13 | this.movement = movement;
14 | }
15 |
16 | @Override
17 | public String getEventName() {
18 | return "RCTSwipeRefreshLayout.TouchMove";
19 | }
20 |
21 | @Override
22 | public void dispatch(RCTEventEmitter rctEventEmitter) {
23 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
24 | }
25 |
26 | private WritableMap serializeEventData() {
27 | WritableMap eventData = Arguments.createMap();
28 | eventData.putInt("movement", getMovement());
29 | return eventData;
30 | }
31 |
32 | private int getMovement() {
33 | return movement;
34 | }
35 |
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/TouchUpEvent.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 | import com.facebook.react.uimanager.events.Event;
4 | import com.facebook.react.uimanager.events.RCTEventEmitter;
5 |
6 | public class TouchUpEvent extends Event {
7 |
8 | public TouchUpEvent(int viewTag, long timestampMs) {
9 | super(viewTag);
10 | }
11 |
12 | @Override
13 | public String getEventName() {
14 | return "RCTSwipeRefreshLayout.TouchUp";
15 | }
16 |
17 | @Override
18 | public void dispatch(RCTEventEmitter rctEventEmitter) {
19 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), null);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativecomponent/swiperefreshlayout/WindowVisibilityChangeEvent.java:
--------------------------------------------------------------------------------
1 | package com.reactnativecomponent.swiperefreshlayout;
2 |
3 | import android.util.Log;
4 |
5 | import com.facebook.react.bridge.Arguments;
6 | import com.facebook.react.bridge.WritableMap;
7 | import com.facebook.react.uimanager.events.Event;
8 | import com.facebook.react.uimanager.events.RCTEventEmitter;
9 |
10 | public class WindowVisibilityChangeEvent extends Event {
11 | boolean hiddenState;
12 |
13 | public WindowVisibilityChangeEvent(int viewTag, long timestampMs, boolean hiddenState) {
14 | // super(viewTag, timestampMs);
15 | super(viewTag);
16 | this.hiddenState = hiddenState;
17 | }
18 |
19 | @Override
20 | public String getEventName(){
21 | return "RCTLayzyLoadView.onWindowVisibilityChange";
22 | }
23 |
24 | @Override
25 | public void dispatch(RCTEventEmitter rctEventEmitter) {
26 | rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
27 | }
28 |
29 | private WritableMap serializeEventData() {
30 | WritableMap eventData = Arguments.createMap();
31 | eventData.putBoolean("hidden",getHiddenState());
32 | // Log.i("Test","hidden="+getHiddenState());
33 |
34 | return eventData;
35 | }
36 |
37 | public boolean getHiddenState() {
38 | return hiddenState;
39 | }
40 |
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SmartSwipeRefreshLayout
3 |
4 |
--------------------------------------------------------------------------------
/constants.js:
--------------------------------------------------------------------------------
1 | export const viewType = {
2 | scrollView: 0,
3 | listView: 1,
4 | }
5 | export const viewState = {
6 | refresh_none: 0,
7 | refresh_idle: 1,
8 | will_refresh: 2,
9 | refreshing: 3,
10 | refresh_freezing: 4,
11 | load_more_none: 5,
12 | load_more_idle: 6,
13 | will_load_more: 7,
14 | loading_more: 8,
15 | load_more_freezing: 9,
16 | loaded_all: 10,
17 | }
18 | export const refreshViewType = {
19 | header: 0,
20 | footer: 1,
21 | }
22 | export const refreshAnimationDuration = 255
23 | export const scrollBounceAnimationDuration = 510
24 |
25 | export default {
26 | viewType,
27 | viewState,
28 | refreshViewType,
29 | refreshAnimationDuration,
30 | scrollBounceAnimationDuration,
31 | }
--------------------------------------------------------------------------------
/easing.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export const easeOutCirc = (x, t, b, c, d) => {
4 | return c * Math.sqrt(1 - (t = t/d - 1) * t) + b;
5 | }
6 |
7 | export default {
8 | easeOutCirc,
9 | }
10 |
11 |
12 |
--------------------------------------------------------------------------------
/note.md:
--------------------------------------------------------------------------------
1 | * 要注意ios由于ScrollView自带bounce空间, 可以不借助native实现
2 | * 要注意android由于ScrollView没有bounce空间, 需要借助自定义native的SwipeRefreshLayout来辅助实现,
3 | 判断scrollView是否拉到顶和底, 然后触发SwipeRefreshLayout的touchmove和touchup事件来控制scrollView的header和footer
4 | * 要注意, 最后一项高度增大/缩小并不会导致当前屏幕视觉上产生偏移
5 | * 要注意, 最后一项高度增大不会导致当前滚动距离产生变化, 但会增加最大滚动距离
6 | * 要注意, 最后一项高度缩小会导致当前滚动距离产生变化, 需要手动修正滚动距离
7 | * 要注意, 第一项高度增大/缩小会导致当前屏幕视频上产生偏移
8 | * 要注意, 第一项高度增大/缩小不会导致当前滚动距离产生变化
9 | * 要注意, 下拉刷新和上拉加载不能同时触发
10 | * 要注意, 如滚动内容高度小于滚动容器高度, 需要补足空白区域
11 | * 要注意, 下拉刷新时, 如上拉加载更多的状态为loaded_all, 需要将其重置为load_more_none
12 | * 要注意, 使用ListView的下拉加载更多功能时, 需要设定pageSize的值每次新增的数据行数量一致, 否则渲染会一行行渲染并导致有可能跳帧
13 | * 要注意, 当使用上拉加载成功加载新数据(比如第二页的数据)后, 再使用下拉刷新时, 由于只加载第一页的数据, 需要在重新获取内容高度后, 额外重置滚动条距离(为只有第一页数据时拉到底的值)
14 | * 要注意, 判断拉到顶时, 仅需验证滚动条距离是否为0,
15 | 判断拉到底时, 需要验证滚动条距离是否等于滚动内容高度减去滚动容器高度, 由于可能存在浮点数, 这里允许有一定误差范围, 范围为正负当前设备的1个像素点px对应1个点的pt值
16 |
17 | * 貌似0.34版本开始ScrollView/ListView在滚动没有结束前, 除了onScroll事件外的其他代码无法执行, 直到滚动(包括动能)完全结束再执行(这将会影响原有代码逻辑)
18 | * 要注意ListView的onEndReached事件为native事件, 使用该原生事件代替在js中手动计算是否拉到底可以完全避免有时连续触发二次上拉加载更多数据逻辑
19 | onEndReachedThreshold设置为0时可能会存在偏差导致onEndReached事件不触发, 改设为StyleSheet.hairlineWidth
20 | * 为节省内存, 为配合react-native-smart-image-loader图片懒加载的使用,
21 | 内嵌新增ListItem, 该组件内部放置listview的行内容,
22 | 当某项ListItem显示在屏幕范围外时, 将行内容置空, 用以最大限度的释放内存,
23 | 当某项ListItem显示在屏幕范围内时, 重新加载行内容
24 | 由于中低端Android普遍渲染速度跟不上, ListItem的整行内容未渲染出来时导致视图上有空白区域, 故增加了允许只指定行内部的子元素(比如Image)进行操作, 其他子元素内容保持不释放
25 | * android实现ios效果的sticky-header, 通过计算显示在可视区域内的顺序第一个section-header的位置,
26 | 以及显示在可视区域内的顺序第一个的row的位置, 来判断当前的sticky-header应设置为哪一个section-header的内容
27 |
28 | * 增加在列表数据(不包括刷新头和加载尾的高度)总高度不足撑满列表容器高度时,
29 | 加入条件判定, 使加载尾不显示, 并且也不会在这种情况下autoLoad.
30 | 直到触发下拉刷新并且列表数据总高度再次不足撑满列表容器高度时, 加载尾才会隐藏
31 | * 修改触发刷新数据方法的时机, 改为等待自定义模拟scrollBack动画结束后才会触发,
32 | (android上拉加载和下拉刷新都是自定义动画, ios只有下拉刷新是自定义动画, 上拉加载沿用了scrollView的native scrollBack效果),
33 | ios额外加入了在符合特定条件的情况下, 在onRespondGrant中也会触发.
34 | (注:原来是在android/_onRefresh, ios/_onRespondRelease方法中触发,
35 | 如触发后返回数据很快, 则调用endRefresh/endLoadMore方法会很快, 这将会导致动画还没执行完就执行其他逻辑, UI有可能会出错)
36 |
37 | * refresh-status:
38 | 0. refresh_none
39 | 1. refresh_idle
40 | 2. will_refresh
41 | 3. refreshing
42 | 4. load_more_none
43 | 5. load_more_idle
44 | 6. will_load_more
45 | 7. loading_more
46 | 8. loaded_all
47 | * pull-down-refresh flow
48 | refresh_none -> refresh_idle -> will_refresh -> refreshing -> refresh_none
49 | * pull-up-load-more flow
50 | load_more_none -> load_more_idle -> will_load_more -> loading_more -> load_more_none
51 | * pull-up-load-more loaded_all flow
52 | load_more_none -> load_more_idle -> will_load_more -> loading_more -> loaded_all
53 | * pull-up-auto-load-more flow
54 | load_more_none -> loading_more -> load_more_none
55 | * pull-up-auto-load-more loaded_all flow
56 | load_more_none -> loading_more -> loaded_all
57 | * prop:
58 | [enum]viewType
59 | [bool]autoLoadMore
60 | [func]onRefresh
61 | [func]onLoadMore
62 | [func]renderHeader
63 | [func]renderFooter
64 | [number]pullUpDistance
65 | [number]pullUpStayDistance
66 | [number]pullDownDistance
67 | [number]pullDownStayDistance
68 | [bool]enabledPullUp
69 | [bool]enabledPullDown
70 | * api:
71 | [func]beginRefresh
72 | [func]endRefresh
73 | [func]endLoadMore
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-smart-pull-to-refresh-listview",
3 | "version": "1.6.11-beta.2",
4 | "description": "A smart pull-down-refresh and pull-up-loadmore react-native listview, for ios, written in pure JS, for android, written in JS and Java.",
5 | "main": "PullToRefreshListView",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview.git"
12 | },
13 | "keywords": [
14 | "react-native",
15 | "smart",
16 | "pull",
17 | "up",
18 | "down",
19 | "refresh",
20 | "loadmore",
21 | "listview",
22 | "component"
23 | ],
24 | "author": "HISAME SHIZUMARU",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview/issues"
28 | },
29 | "homepage": "https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview#readme",
30 | "dependencies": {
31 | "react-native-smart-timer-enhance": "^1.0.2",
32 | "prop-types": "^15.5.10"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 |
2 | import { StyleSheet, } from 'react-native'
3 |
4 | export const withinErrorMargin = (left, right, threshold = 0) => {
5 | return Math.abs(left - right) < (StyleSheet.hairlineWidth + threshold)
6 | }
7 |
8 | export default {
9 | withinErrorMargin,
10 | }
--------------------------------------------------------------------------------