├── README.md ├── index.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # React Native scrollview lazyload 2 | ScrollView with image/components lazyload feature. Support all ScrollView feature. Detect ScrollView by 'renderRow' and 'dataSource' props(ListView should be add this props). 3 | 4 | 5 | # Component Params 6 | | param | type | description | 7 | | --- | --- | --- | 8 | | autoTriggerIsInScreen | boolean | default: false. Is auto trigger isInScreen to elements | 9 | | autoTriggerIsInScreenTime | number | default: 1500. unit: ms | 10 | | lazyExtra | number | default: 1000,Setup lasy load area. (doesn't include screen height) | 11 | 12 | # LazyloadView children components params 13 | | children props | type | description | 14 | | --- | --- | --- | 15 | | lazyloadSrc | string, object | default: none. Image component should be `` or ``| 16 | | lazyRender | boolean | default: false. Using lazy reader feature. When components in screen will trigger `setState({ isRendered: true })` for component. Only trigger once. | 17 | | lazyInScreen | boolean | default: false. When components in screen will trigger `setState({ isInScreen: true })`,Only trigger once. | 18 | 19 | # how to use 20 | `npm install react-native-scrollview-lazyload --save` 21 | 22 | * Lazy load image: `` 23 | * Lazy load image: `` 24 | * Lazy load components: `` 25 | * Trigger components in screen: `` 26 | * Lazy load area(default is 1000): `` 27 | 28 | ```javascript 29 | var React = require('react-native'); 30 | var { 31 | AppRegistry, 32 | Text, 33 | View, 34 | Image, 35 | ListView, 36 | } = React; 37 | 38 | var LazyComponent = React.createClass({ 39 | getDefaultProps: function() { 40 | return { 41 | lazyRender: false 42 | }; 43 | }, 44 | getInitialState: function() { 45 | return { 46 | isRendered: !this.props.lazyRender, 47 | }; 48 | }, 49 | shouldComponentUpdate: function() { 50 | // forceRender is dont used lazy render 51 | if(this.state.isRendered && !this.props.forceRender) { 52 | return false; 53 | } 54 | 55 | return true; 56 | }, 57 | renderLoading: function() { 58 | return ( 59 | {'Loading...'} 60 | ); 61 | }, 62 | render: function(){ 63 | var content = ({this.props.children}) || null; 64 | 65 | if(!this.state.isRendered && this.props.lazyRender) { 66 | content = this.renderLoading(); 67 | } 68 | 69 | return ( 70 | 71 | {content} 72 | 73 | ); 74 | }, 75 | }); 76 | 77 | var InScreenComponents = React.createClass({ 78 | shouldComponentUpdate: function() { 79 | // forceRender is dont used lazy render 80 | if(this.state.isInScreen) { 81 | this.setState({ title: 'Is in screen first time'}) 82 | } 83 | 84 | return false; 85 | }, 86 | render: function(){ 87 | return ( 88 | 89 | {this.state.title || 'Never in screen'} 90 | 91 | ); 92 | }, 93 | }); 94 | 95 | var LazyloadView = require('@ali/react-native-lazyloadview'); 96 | 97 | ... 98 | 99 | render: function() { 100 | // ListView 101 | var body = [], 102 | rowStyle = { 103 | flex:1, 104 | height: 60, 105 | alignItems: 'center', 106 | justifyContent: 'center', 107 | flexDirection: 'row', 108 | }, 109 | renderElement = function(d){ 110 | return ( 111 | 112 | 121 | Row: {d.index} 122 | 123 | ); 124 | }; 125 | 126 | 127 | 128 | for(var i = 0 ; i < 100; i++) { 129 | body.push({ 130 | ref: 'row' + i, 131 | img: 'https://placeholdit.imgix.net/~text?txtsize=8&txt=60%C3%9760&w=60&h=60', 132 | index: i, 133 | }); 134 | } 135 | 136 | return ( 137 | { 141 | // console.log('renderRow = ', rowData, rowID); 142 | return renderElement(rowData); 143 | }} 144 | _onScroll={(e) => { 145 | console.log('_onScroll = ', e.nativeEvent); 146 | }} 147 | > 148 | ); 149 | 150 | 151 | // ScrollView 152 | var body = [], 153 | rowStyle = { 154 | flex:1, 155 | height: 60, 156 | alignItems: 'center', 157 | justifyContent: 'center', 158 | flexDirection: 'row', 159 | }, 160 | imgStyle = { 161 | width: 60, 162 | height: 60, 163 | position: 'absolute', 164 | left: 0, 165 | }; 166 | 167 | for(var i = 0 ; i < 100; i++) { 168 | var imageUrl = 'https://placeholdit.imgix.net/~text?txtsize=8&txt=60%C3%9760&w=60&h=60'; 169 | body.push( 170 | 171 | 175 | Row: {i} 176 | 177 | 178 | 179 | 180 | 181 | ); 182 | } 183 | 184 | return ( 185 | { 187 | 188 | }}> 189 | _onScrollEndDrag={(e) => { 190 | 191 | }} 192 | _renderHeader={(e) => { 193 | // when add this props please return something 194 | return (HEADER); 195 | }} 196 | {body} 197 | 198 | ); 199 | }, 200 | 201 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react-native'); 4 | 5 | var { 6 | ScrollView, 7 | NativeModules, 8 | } = React; 9 | 10 | var RCTUIManager = NativeModules.UIManager; 11 | 12 | var ScrollViewComponent = React.createClass({ 13 | 14 | mixins:[React.addons.PureRenderMixin], 15 | 16 | _toLoadImages: true, 17 | 18 | _merge: function(originObj, replaceObj) { 19 | for(var i in replaceObj) { 20 | originObj[i] = replaceObj[i]; 21 | } 22 | 23 | return originObj; 24 | }, 25 | 26 | loadVisibleImages: function(){ 27 | // start auto trigger isInScreen 28 | this.autoTriggerIsInScreenStart(); 29 | 30 | if(!this._toLoadImages) { 31 | return; 32 | } 33 | 34 | requestAnimationFrame(() => { 35 | var scrollView = this.getScrollResponder(); 36 | if(scrollView){ 37 | var scrollInner = scrollView._reactInternalInstance._renderedComponent._renderedChildren['.0']; 38 | if(__DEV__){ 39 | scrollInner = scrollInner._renderedComponent; 40 | } 41 | 42 | //traverse scroll contents 43 | this._traverseAllChildren(scrollInner._renderedChildren); 44 | } 45 | }); 46 | }, 47 | 48 | _traverseAllChildren: function(children) { 49 | var _this = this; 50 | 51 | for(var i in children){ 52 | var child = children[i], 53 | renderedComponent = child._renderedComponent; 54 | 55 | if(!renderedComponent || typeof(child._currentElement) === 'string'){ 56 | if(!__DEV__ && child._renderedChildren) { 57 | this._traverseAllChildren(child._renderedChildren); 58 | } 59 | continue; 60 | } 61 | 62 | if(child._instance.props.lazySkip === true) { 63 | continue; 64 | } 65 | 66 | // trigger only once 67 | if(child._instance.props.lazyInScreen === true) { 68 | if(!child._instance.state || !!!child._instance.state.isInScreen) { 69 | _this._renderedLazyInScreen(child); 70 | } 71 | } 72 | 73 | // trigger only once 74 | if(child._instance.props.lazyRender === true && !child._instance.state.isRendered) { 75 | _this._renderedLazyComponent(child); 76 | continue; 77 | } 78 | 79 | if(child._instance.props.lazyloadSrc && (!child._instance.state || !child._instance.state.isLoaded)) { 80 | _this._checkImageToLoad(renderedComponent, child); 81 | continue; 82 | } 83 | 84 | // when elements doesn't in screen skip children 85 | while(renderedComponent) { 86 | if(renderedComponent._renderedChildren) { 87 | _this._traverseAllChildren(renderedComponent._renderedChildren); 88 | break; 89 | }else{ 90 | renderedComponent = renderedComponent._renderedComponent; 91 | } 92 | } 93 | } 94 | }, 95 | 96 | _checkImageToLoad: function(instance, component){ 97 | var renderedComponent = instance._renderedComponent || instance, 98 | elem = renderedComponent._currentElement, 99 | lazyloadSrc = elem.props.lazyloadSrc, 100 | instance = elem._owner._instance; 101 | 102 | // image is loaded 103 | if(lazyloadSrc && (!instance.state || !instance.state.isLoaded)){ 104 | var scrollView = this.getScrollResponder(), 105 | scrollY = this.scrollProperties.offsetY || 0; 106 | 107 | this.getPosition(scrollView, instance, scrollY) 108 | .then((res) => { 109 | if(res && res.isInScreen) { 110 | var lazySrc = instance.props.lazyloadSrc, 111 | src = {uri: ''}; 112 | 113 | if(Object.prototype.toString.call(lazySrc) === '[object String]'){ 114 | src = { 115 | uri: lazySrc 116 | }; 117 | }else if(Object.prototype.toString.call(lazySrc) === '[object Object]'){ 118 | src = { 119 | uri: (lazySrc.uri) ? lazySrc.uri : '' 120 | }; 121 | } 122 | 123 | component._setPropsInternal({source: src}); 124 | instance.setState({ 125 | isLoaded: true 126 | }); 127 | } 128 | }); 129 | } 130 | }, 131 | 132 | _renderedLazyComponent: function(instance) { 133 | var instance = instance._instance; 134 | 135 | if(instance.props.lazyRender === true && !instance.state.isRendered) { 136 | var scrollView = this.getScrollResponder(), 137 | scrollY = this.scrollProperties.offsetY || 0; 138 | 139 | // using cache 140 | // disable it. because sometime can not control floor height. calc ti every time 141 | 142 | this.getPosition(scrollView, instance, scrollY) 143 | .then((res) => { 144 | if(res) { 145 | instance.cachePosition = res.cachePosition; 146 | instance.setState({ 147 | isRendered: res.isInScreen 148 | }); 149 | } 150 | }); 151 | } 152 | }, 153 | 154 | _renderedLazyInScreen: function(instance) { 155 | var instance = instance._instance; 156 | 157 | if(instance.props.lazyInScreen === true) { 158 | if(!instance.state || !!!instance.state.isInScreen) { 159 | var scrollView = this.getScrollResponder(), 160 | pos = instance.cachePosition, 161 | scrollY = this.scrollProperties.offsetY || 0; 162 | 163 | // using cache 164 | // disable it. because sometime can not control floor height. calc ti every time 165 | if(pos && instance.state && this.checkInScreen(pos)) { 166 | instance.setState({ 167 | isInScreen: true, 168 | }); 169 | return; 170 | } 171 | 172 | this.getPosition(scrollView, instance, scrollY) 173 | .then((res) => { 174 | if(res) { 175 | instance.cachePosition = res.cachePosition; 176 | instance.setState({ 177 | isInScreen: res.isInScreen 178 | }); 179 | } 180 | }); 181 | } 182 | } 183 | }, 184 | 185 | getPosition: function(scrollView, instance, scrollY) { 186 | scrollView = scrollView || this.getScrollResponder(); 187 | var _this = this, 188 | scrollProperties = this.scrollProperties; 189 | 190 | return new Promise(function(resolve, reject) { 191 | try { 192 | RCTUIManager.measureLayout( 193 | React.findNodeHandle(instance), 194 | scrollView.getInnerViewNode(), 195 | null, 196 | (left, top, width, height) => { 197 | var cachePos = { 198 | left: left, 199 | top: top, 200 | width: width, 201 | height: height, 202 | }; 203 | 204 | resolve({ 205 | isInScreen: _this.checkInScreen(cachePos), 206 | cachePosition: cachePos 207 | }); 208 | } 209 | ); 210 | }catch (e) { 211 | reject(null); 212 | } 213 | }); 214 | }, 215 | 216 | checkInScreen: function(pos) { 217 | var _left = pos.left, 218 | _top = pos.top, 219 | _width = pos.width, 220 | _height = pos.height; 221 | 222 | var _isInScreen = false, 223 | scrollProperties = this.scrollProperties, 224 | scrollY = scrollProperties.offsetY || 0, 225 | screenTop = scrollY - this.props.lazyExtra, 226 | screenBottom = scrollY + scrollProperties.visibleHeight + this.props.lazyExtra; 227 | 228 | //check Image is visible 229 | if(_top + _height > screenTop && _top < screenBottom){ 230 | _isInScreen = true; 231 | } 232 | 233 | return _isInScreen; 234 | }, 235 | 236 | getDefaultProps: function() { 237 | return { 238 | ref: 'lazyloadView', 239 | scrollEventThrottle: 80, 240 | _onMomentumScrollDelay: 400, 241 | _onScrollEndDelay: 100, 242 | lazyExtra: 1000, 243 | autoTriggerIsInScreenTime: 1500, 244 | autoTriggerIsInScreen: false, 245 | renderHeader: function(){ return null; }, 246 | renderFooter: function(){ return null; }, 247 | }; 248 | }, 249 | 250 | componentWillMount: function(){ 251 | this.scrollProperties = { 252 | contentHeight: 0, 253 | visibleHeight: 0, 254 | offsetY: 0, 255 | }; 256 | 257 | this.autoTriggerIsInScreenEnd = false; 258 | }, 259 | 260 | componentDidMount: function(){ 261 | this._reactInternalInstance._setPropsInternal({ 262 | onScroll: this._onScroll, 263 | onScrollEndDrag: this._onScrollEndDrag, 264 | onMomentumScrollBegin: this._onMomentumScrollBegin, 265 | onMomentumScrollEnd: this._onMomentumScrollEnd, 266 | }); 267 | 268 | requestAnimationFrame(() => { 269 | this._measureScrollProperties(); 270 | 271 | // when init trigger once 272 | this.loadVisibleImages(); 273 | }); 274 | 275 | this.autoTriggerIsInScreenStart(); 276 | }, 277 | 278 | _measureScrollProperties: function(){ 279 | var scrollView = this.getScrollResponder(); 280 | try { 281 | RCTUIManager.measureLayout( 282 | scrollView.getInnerViewNode(), 283 | React.findNodeHandle(scrollView), 284 | null, 285 | (left, top, width, height) => { 286 | this.scrollProperties.contentHeight = height; 287 | } 288 | ); 289 | 290 | RCTUIManager.measureLayoutRelativeToParent( 291 | React.findNodeHandle(scrollView), 292 | null, 293 | (left, top, width, height) => { 294 | this.scrollProperties.visibleHeight = height; 295 | this.loadVisibleImages(); 296 | } 297 | ); 298 | 299 | }catch (e) {} 300 | }, 301 | 302 | // check is ListView or ScrollView 303 | getScrollResponder: function(){ 304 | var scrollView = this.refs[this.props.ref]; 305 | return scrollView && scrollView.getScrollResponder ? scrollView.getScrollResponder() : scrollView; 306 | }, 307 | 308 | render: function(){ 309 | var content = [ 310 | this.props.renderHeader(), 311 | this.props.children, 312 | this.props.renderFooter(), 313 | ], 314 | LazyloadView = ScrollView; 315 | 316 | return ( 317 | 320 | {content} 321 | 322 | ); 323 | }, 324 | 325 | _onScroll: function(e){ 326 | var scrollProperties = this.scrollProperties; 327 | scrollProperties.visibleHeight = e.nativeEvent.layoutMeasurement.height; 328 | scrollProperties.contentHeight = e.nativeEvent.contentSize.height; 329 | scrollProperties.offsetY = e.nativeEvent.contentOffset.y; 330 | 331 | this.lastScrollEvent = e.nativeEvent; 332 | 333 | // `onEndReached` polyfill for ScrollView 334 | if(this.props.onEndReached){ 335 | var onEndReached_Threshold = this.props.onEndReachedThreshold || 1000; 336 | var nearEnd = scrollProperties.contentHeight - scrollProperties.visibleHeight - scrollProperties.offsetY < onEndReachedThreshold; 337 | if(nearEnd && scrollProperties.contentHeight !== this._sentEndForContentHeight){ 338 | this._sentEndForContentHeight = scrollProperties.contentHeight; 339 | this.props.onEndReached(e); 340 | } 341 | } 342 | 343 | this.loadVisibleImages(); 344 | 345 | this.props._onScroll && this.props._onScroll(e); 346 | 347 | this._scrollEndTimer && clearTimeout(this._scrollEndTimer); 348 | this._scrollEndTimer = setTimeout(() => { 349 | this._scrollEndTimer = null; 350 | this.props.onScrollEnd && this.props.onScrollEnd(e); 351 | }, this.props._onScrollEndDelay); 352 | }, 353 | 354 | _onScrollEndDrag: function(e){ 355 | this._loadId && clearTimeout(this._loadId); 356 | this._loadId = setTimeout(() => { 357 | this._loadId = null; 358 | this._toLoadImages = true; 359 | }, this.props._onMomentumScrollDelay); 360 | 361 | this.props._onScrollEndDrag && this.props._onScrollEndDrag(e); 362 | }, 363 | 364 | _onMomentumScrollBegin: function(e){ 365 | this._toLoadImages = false; 366 | 367 | this.props._onMomentumScrollBegin && this.props._onMomentumScrollBegin(e); 368 | }, 369 | 370 | _onMomentumScrollEnd: function(e){ 371 | this.loadVisibleImages(); 372 | 373 | this.props._onMomentumScrollEnd && this.props._onMomentumScrollEnd(e); 374 | }, 375 | 376 | // auto pre trigger isInScreen:: start 377 | autoTriggerIsInScreenStart: function() { 378 | if(this.props.autoTriggerIsInScreen) { 379 | if(this.autoTriggerIsInScreenEnd) { 380 | return; 381 | } 382 | 383 | // stop when timer is running 384 | this.autoTriggerIsInScreenStop(); 385 | 386 | this.autoTriggerIsInScreenTimer = setTimeout(() => { 387 | try { 388 | var scrollView = this.getScrollResponder(); 389 | 390 | if(scrollView){ 391 | var scrollInner = scrollView._reactInternalInstance._renderedComponent._renderedChildren['.0']; 392 | 393 | if(__DEV__){ 394 | scrollInner = scrollInner._renderedComponent; 395 | } 396 | 397 | //traverse scroll contents 398 | var children = scrollInner._renderedChildren, 399 | isHasInScreenEl = false; 400 | 401 | for(var i in children) { 402 | var child = children[i], 403 | renderedComponent = child._renderedComponent; 404 | 405 | if(!renderedComponent || typeof child._currentElement === 'string'){ 406 | continue; 407 | } 408 | 409 | // trigger only once 410 | if(child._instance.props.lazyInScreen === true) { 411 | if(!child._instance.state || !!!child._instance.state.isInScreen) { 412 | 413 | isHasInScreenEl = true; 414 | 415 | child._instance.setState({ 416 | isInScreen: true, 417 | }); 418 | break; 419 | } 420 | } 421 | } 422 | 423 | // when is End don't run this again 424 | if(isHasInScreenEl) { 425 | this.autoTriggerIsInScreenStart(); 426 | }else { 427 | this.autoTriggerIsInScreenEnd = true; 428 | } 429 | } 430 | } catch(e) {} 431 | }, this.props.autoTriggerIsInScreenTime); 432 | } 433 | }, 434 | 435 | // auto pre trigger isInScreen:: stop 436 | autoTriggerIsInScreenStop: function() { 437 | if(this.props.autoTriggerIsInScreen) { 438 | if(this.autoTriggerIsInScreenTimer) { 439 | clearTimeout(this.autoTriggerIsInScreenTimer); 440 | this.autoTriggerIsInScreenTimer = null; 441 | } 442 | } 443 | }, 444 | 445 | // force load images/ components 446 | refresh: function() { 447 | this.loadVisibleImages(); 448 | }, 449 | }); 450 | 451 | 452 | module.exports = ScrollViewComponent; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-scrollview-lazyload", 3 | "version": "1.0.5", 4 | "description": "react-native scrollview with image lazy load", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/IskenHuang/react-native-scrollview-lazyload.git" 12 | }, 13 | "keywords": [ 14 | "react-native", 15 | "scrollview", 16 | "lazyload", 17 | "image" 18 | ], 19 | "author": "Isken Huang", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/IskenHuang/react-native-scrollview-lazyload/issues" 23 | }, 24 | "homepage": "https://github.com/IskenHuang/react-native-scrollview-lazyload#readme" 25 | } 26 | --------------------------------------------------------------------------------