├── .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 | [![npm](https://img.shields.io/npm/v/react-native-smart-pull-to-refresh-listview.svg)](https://www.npmjs.com/package/react-native-smart-pull-to-refresh-listview) 4 | [![npm](https://img.shields.io/npm/dm/react-native-smart-pull-to-refresh-listview.svg)](https://www.npmjs.com/package/react-native-smart-pull-to-refresh-listview) 5 | [![npm](https://img.shields.io/npm/dt/react-native-smart-pull-to-refresh-listview.svg)](https://www.npmjs.com/package/react-native-smart-pull-to-refresh-listview) 6 | [![npm](https://img.shields.io/npm/l/react-native-smart-pull-to-refresh-listview.svg)](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 | } --------------------------------------------------------------------------------