react-native ListView 封装 实现 下拉刷新/上拉加载更多
1.PageListView 组件封装
src/components/PageListView/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 | /** * 上拉刷新/下拉加载更多 组件 */ import React, { Component } from 'react' ; import { Text, View, ListView, FlatList, Dimensions, PanResponder, Animated, Easing, ActivityIndicator, } from 'react-native' ; let PageList=FlatList||ListView; //获取屏幕宽高 let {width:w, height:h}=Dimensions.get( 'window' ); //pullState对应的相应的文字说明 const pullStateTextArr={ 'noPull' : '' , 'pulling' : '下拉刷新...' , 'pullOk' : '释放以刷新...' , 'pullRelease' : '正在刷新,请稍等...' , }; //默认动画时长 const defaultDuration=400; //1.0.3->1.1.0改动/新增: /* 1.手动处理数组数据, 2.父组件重新加载数据后手动刷新数据 2.隐藏当前ListView(放弃这个功能), 3.从网络获取数据,数据为空时的渲染界面, 4.解决部分手机上界面为空,不显示的问题,(鉴于自定义组件宽高实用性并不大,而且部分手机显示有问题,去除自定义组件宽高,改为自适应)(问题可能原因:从flex:1快速的改变为固定宽高时界面渲染会有问题) 5.对放在scrollView中的支持 6.加入可选属性allLen,对于分页显示时可以指定数据的总条数 */ export default class PageListView extends Component{ constructor(props){ super (props); this .state={ //DataSource数据源对应的数组数据 dataArr:[], //ListView的数据源 dataSource: this .props.isListView? new ListView.DataSource({ rowHasChanged: (r1, r2)=>r1 !== r2 }):[], //下面两个参数来决定是否可以调用加载更多的方法 //ListView/FlatView中标识是否可以加载更多(当现在获取到的数据已经是全部了,不能再继续获取数据了,则设为false,当还有数据可以获取则设为true) canLoad: false , //标识现在是否ListView/FlatView现在正在加载(根据这个值来决定是否显示"正在加载的cell")(loadMore()方法进去后设为true,fetch加载完数据后设为false) isLoadding: false , //是否显示下拉刷新的cell ifShowRefresh: false , //ListView/FlatList是否可以滚动 scrollEnabled: true , //记录当前加载到了哪一页 page:2, //通过View自适应的宽高来决定ListView的宽高(或让用户来决定宽高) // width:this.props.width||0, // height:this.props.height||0, width:0, height:0, //下拉的状态 pullState: 'noPull' , pullAni: new Animated.Value(- this .props.renderRefreshViewH), //网络获取的数据是否为空 ifDataEmpty: false , }; //创建手势相应者 this .panResponder = PanResponder.create({ onMoveShouldSetPanResponder: this .onMoveShouldSetPanResponder, onPanResponderMove: this .onPanResponderMove, onPanResponderRelease: this .onPanResponderRelease, onPanResponderTerminate: this .onPanResponderRelease, onShouldBlockNativeResponder: ()=> false }); //下拉到什么位置时算拉到OK的状态 this .pullOkH=parseInt( this .props.renderRefreshViewH*1.5); //记录ListView最后一次滚动停止时的y坐标 this .lastListY=0; } static defaultProps={ //当前控件是否为ListView isListView:PageList===ListView, //父组件处理"渲染FlatList/ListView的每一行"的方法 renderRow: null , //父组件处理"下拉刷新"或"一开始加载数据"的方法 refresh: null , //父组件处理"加载更多"的方法 loadMore: null , //每个分页的数据数 pageLen:0, //总的数据条数 allLen:0, //如果父组件中包含绝对定位的View时传入ListView的高度 //或者可以在父组件底部加入相应高度的透明View // height:0, // width:0, //如果需要在用当前后端返回的数组数据进行处理的话,传入回调函数 dealWithDataArrCallBack: null , //如果在进行某个操作后需要对数组数据进行手动处理的话,传入回调函数 // changeDataArr:null, //渲染每行View之间的分割线View ItemSeparatorComponent: null , //还有数据可以从后端取得时候渲染底部View的方法 renderLoadMore: null , //没有数据(数据已经从后端全部加载完)是渲染底部View的方法 renderNoMore: null , //渲染下拉刷新的View样式 renderRefreshView: null , //渲染下拉刷新的View样式的高度 renderRefreshViewH:60, //如果网络获取数据为空时的渲染界面 renderEmpty: null , //当前组件是否是放在scrollView中(放在ScrollView中时则不能上拉刷新,下拉加载更多) inScrollView: false , //是否隐藏当前ListView // ifHide:false, }; //取到View自适应的宽高设置给ListView onLayout=(event)=>{ if ( this .state.width&& this .state.height){ return } let {width:w, height:h} = event.nativeEvent.layout; this .setState({width:w,height:h}); }; render() { if ( this .state.ifDataEmpty&& this .props.renderEmpty){ return this .props.renderEmpty()} if ( this .props.inScrollView){ return this .renderListView()} return ( <View style={[{flex:1},{zIndex:-99999}]} onLayout={ this .onLayout}> <Animated.View ref={aniView=>{ this .aniView=aniView}} style={[{transform:[{translateY: this .state.pullAni}]},{width: this .state.width,height: this .state.height+ this .props.renderRefreshViewH}]}> { this .props.renderRefreshView? this .props.renderRefreshView( this .state.pullState): this .renderRefreshView()} <View style={[{width: this .state.width,height: this .state.height}]} {... this .panResponder.panHandlers}> { this .renderListView()} </View> </Animated.View> </View> ); } //ListView/FlatList的渲染 renderListView=()=>{ if (! this .props.isListView){ if ( this .props.pageLen){ return ( <PageList {... this .props} style={{}} //虽然不需要样式,但必须加,这样才能在视图更新时调用renderFooter方法 data={ this .state.dataSource} //当接近ListView的底部时的操作 onEndReached={ this .willReachEnd} //当距离底部多少距离时触发上面的这个方法 注意:在FlatList中此参数是一个比值而非像素单位。比如,0.5表示距离内容最底部的距离为当前列表可见长度的一半时触发 onEndReachedThreshold={0.05} //渲染加载更多时,"加载中"的cell ListFooterComponent={ this .renderFooter} //渲染每一行的cell怎么样显示 renderItem={ this .renderItem} keyExtractor={(item,index)=>index.toString()} scrollEnabled={ this .state.scrollEnabled} onScroll={ this .onScroll} ref={list=>{ this .list=list}} /> ); } else { return ( <PageList {... this .props} style={{}} //虽然不需要样式,但必须加,这样才能在视图更新时调用renderFooter方法 data={ this .state.dataSource} //渲染每一行的cell怎么样显示 renderItem={ this .renderItem} ItemSeparatorComponent={ this .renderItemS} keyExtractor={(item,index)=>index.toString()} /> ); } } else { if ( this .props.pageLen){ return ( <PageList {... this .props} style={{}} //虽然不需要样式,但必须加,这样才能在视图更新时调用renderFooter方法 dataSource={ this .state.dataSource} //当接近ListView的底部时的操作 onEndReached={ this .willReachEnd} //当距离底部多少距离时触发上面的这个方法 onEndReachedThreshold={10} //渲染加载更多时,"加载中"的cell renderFooter={ this .renderFooter} //渲染每一行的cell怎么样显示 renderRow={ this .renderRow} //允许空的组,加上就行(不用管) enableEmptySections={ true } scrollEnabled={ this .state.scrollEnabled} onScroll={ this .onScroll} ref={list=>{ this .list=list}} /> ); } else { return ( <PageList {... this .props} style={{}} //虽然不需要样式,但必须加,这样才能在视图更新时调用renderFooter方法 dataSource={ this .state.dataSource} //渲染每一行的cell怎么样显示 renderRow={ this .renderRow} //允许空的组,加上就行(不用管) enableEmptySections={ true } /> ); } } }; componentDidMount(){ this .resetAni(); this .props.refresh((res)=>{ if (! this .dealWithArr(res)){ return } let len=res.length; this .updateData(res,len); }); } //当快要接近底部时加载更多 willReachEnd=()=> { if ( this .state.canLoad && ! this .state.isLoadding) { this .loadMore(); } }; //加载更多 loadMore=()=>{ this .setState({isLoadding: true }); let page = this .state.page; this .props.loadMore(page,(res)=>{ let len=res.length; this .setState({isLoadding: false ,page: this .state.page+1}); this .updateData(res,len, true ); }); }; //刷新 refreshCommon=(res)=>{ if (! this .dealWithArr(res)){ return } let len=res.length; this .updateData(res,len); this .setState({page:2,ifShowRefresh: false ,pullState: 'noPull' }); this .resetAni() }; //下拉刷新 refresh=()=>{ this .props.refresh((res)=>{ this .refreshCommon(res) }); }; //手动刷新 manualRefresh=(res)=>{ this .refreshCommon(res); }; //判断传入的数据是否为数组,或数组是否为空 dealWithArr=(res)=>{ let isArr=Array.isArray(res); if (!isArr){ this .setState({ifDataEmpty: true });console.warn( 'PageListView的数据源需要是一个数组' ); return false ;} let len=res.length; if (!len){ this .setState({ifDataEmpty: true }); return false ;} return true ; }; //ListView渲染每一行的cell renderRow=(rowData,group,index)=>{ let {renderRow,ItemSeparatorComponent,pageLen,allLen}= this .props; let notLast=parseInt(index)!== this .state.dataArr.length-1; let ifRenderItemS= false ; if (ItemSeparatorComponent){ if (allLen){ ifRenderItemS=parseInt(index)!==allLen-1; } else { ifRenderItemS=(pageLen&&( this .state.canLoad||notLast))||(!pageLen&¬Last); } } // let ifRenderItemS=this.props.ItemSeparatorComponent&&((this.props.pageLen&&(this.state.canLoad||notLast))||(!this.props.pageLen&¬Last)); return (<View>{renderRow(rowData,index)}{ifRenderItemS&&ItemSeparatorComponent()}</View>); }; //FlatList渲染每一行的cell renderItem=({item,index})=>{ return this .props.renderRow(item,index); }; //渲染cell之间的分割线组件 renderItemS=()=>{ return this .props.ItemSeparatorComponent&& this .props.ItemSeparatorComponent(); }; //正在加载的cell renderFooter=()=>{ if (! this .state.canLoad) { if ( this .props.renderNoMore){ return this .props.renderNoMore(); } else { return ( <View style={{alignItems: 'center' , justifyContent: 'center' ,height:40,width:w,backgroundColor: '#eee' }}> <Text allowFontScaling={ false } style={{color: '#000' , fontSize: 12}}>没有更多数据了...</Text> </View> ); } } else { if ( this .props.renderLoadMore){ return this .props.renderLoadMore(); } else { return ( <View style={{alignItems: 'center' , justifyContent: 'center' ,height:40,width:w,backgroundColor: '#eee' ,flexDirection: 'row' }}> <ActivityIndicator animating={ this .state.isLoadding} color= '#333' size= 'small' style={{marginRight:7}}/> <Text allowFontScaling={ false } style={{color: '#000' , fontSize: 12,}}>{ this .state.isLoadding? '正在加载中,请稍等' : '上拉加载更多' }...</Text> </View> ); } } }; //更新状态机 updateData=(res,len,loadMore= false )=>{ let dataArr=[]; let {pageLen,allLen}= this .props; if (loadMore){ for ( let i=0;i<len;i++){ this .state.dataArr.push(res[i]); } } else { this .state.dataArr=res; } !! this .props.dealWithDataArrCallBack?(dataArr= this .props.dealWithDataArrCallBack( this .state.dataArr)):dataArr= this .state.dataArr; this .setState({ dataArr:dataArr, dataSource: this .props.isListView? this .state.dataSource.cloneWithRows(dataArr):dataArr, canLoad:allLen?(allLen> this .state.dataArr):(pageLen?(len===pageLen): false ), }); }; //如果在进行某个操作后需要对数组数据进行手动处理的话,调用该方法(通过ref来调用refs={(r)=>{!this.PL&&(this.PL=r)}}) changeDataArr=(callBack)=>{ let arr=JSON.parse(JSON.stringify( this .state.dataArr)); let dataArr=callBack(arr); this .setState({ dataArr:dataArr, dataSource: this .props.isListView? this .state.dataSource.cloneWithRows(dataArr):dataArr, }); }; //ListView/FlatList滚动时的方法 onScroll=(e)=>{ this .lastListY=e.nativeEvent.contentOffset.y; this .lastListY<=0&& this .setState({scrollEnabled: false }) }; //开始移动时判断是否设置当前的View为手势响应者 onMoveShouldSetPanResponder=(e,gesture)=> { if (! this .props.pageLen) return false ; let {dy}=gesture; let bool; if (dy<0){ //向上滑 if ( this .state.pullState!== 'noPull' ){ this .resetAni(); } ! this .state.scrollEnabled&& this .setState({scrollEnabled: true }); bool= false ; } else { //向下拉 if ( this .state.pullState!== 'noPull' ){ bool= true ; } else { bool=! this .state.scrollEnabled|| this .lastListY<1; } } return bool; }; //手势响应者的View移动时 onPanResponderMove=(e,gesture)=>{ this .dealWithPan(e,gesture); }; dealWithPan=(e,gesture)=>{ let {dy}=gesture; if (dy<0){ //向上滑 if ( this .state.pullState!== 'noPull' ){ this .resetAni(); } else { ! this .state.scrollEnabled&& this .setState({scrollEnabled: true }) } } else { //向下拉 let pullDis=gesture.dy/2; let pullOkH= this .pullOkH; let aniY=pullDis- this .props.renderRefreshViewH; this .state.pullAni.setValue(aniY); if (pullDis>pullOkH){ this .setState({pullState: 'pullOk' }) } else if (pullDis>0){ this .setState({pullState: 'pulling' }) } } }; //手势响应者被释放时 onPanResponderRelease=(e,gesture)=>{ switch ( this .state.pullState){ case 'pulling' : this .resetAni(); this .setState({scrollEnabled: true }); break ; case 'pullOk' : this .resetAniTop(); this .setState({pullState: 'pullRelease' ,scrollEnabled: true }); this .refresh(); break ; } }; //重置位置 refreshView刚好隐藏的位置 resetAni=()=>{ this .setState({pullState: 'noPull' }); // this.state.pullAni.setValue(this.defaultXY); this .resetList(); Animated.timing( this .state.pullAni, { toValue: - this .props.renderRefreshViewH, // toValue: this.defaultXY, easing: Easing.linear, duration: defaultDuration/2 }).start(); }; //重置位置 refreshView刚好显示的位置 resetAniTop=()=>{ this .resetList(); Animated.timing( this .state.pullAni, { toValue: 0, // toValue: {x:0,y:0}, easing: Easing.linear, duration: defaultDuration/2 }).start(); }; //重置ListView/FlatList位置 resetList=()=>{ this .list&&( this .props.isListView? this .list.scrollTo({y:0}): this .list.scrollToOffset({offset:0})); }; //滚动ListView/FlatList位置 scrollList=(y)=>{ this .list&&( this .props.isListView? this .list.scrollTo({y:y}): this .list.scrollToOffset({offset:y})); }; //渲染默认的下拉刷新View renderRefreshView=()=>{ return ( <View style={{height:60,width:w,justifyContent: 'center' ,alignItems: 'center' ,backgroundColor: '#eee' ,flexDirection: 'row' }}> <ActivityIndicator animating={ this .state.pullState=== 'pullRelease' } color= '#333' size= 'small' style={{marginRight:7}}/> <Text allowFontScaling={ false } style={{color: '#333' ,fontSize:15}}>{pullStateTextArr[ this .state.pullState]}</Text> </View> ); }; } |
2.页面调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <PageListView pageLen={10} renderRow={ this ._renderRow.bind( this )} refresh={ this ._refresh.bind( this )} loadMore={ this ._loadMore.bind( this )} /> // 20180730 刷新 _refresh(callBack){ // fetch(分页接口url+'?page=1') // .then((response)=>response.json()) // .then((responseData)=>{ // //根据接口返回结果得到数据数组 // let arr=responseData.result; // callBack(arr); // }); request .get(config.api.base + config.api.comment, { accessToken: 'abc' , page: 1, creation: '123' }) .then((data) => { //根据接口返回结果得到数据数组 let arr = data.data; callBack(arr); }) . catch ((error) => { console.log( '请求失败!' ); }) } // 20180730 加载更多 _loadMore(page,callBack){ // fetch(分页接口url+'?page='+page) // .then((response)=>response.json()) // .then((responseData)=>{ // //根据接口返回结果得到数据数组 // let arr=responseData.result; // callBack(arr); // }); request .get(config.api.base + config.api.comment, { accessToken: 'abc' , page: page, creation: '123' }) .then((data) => { //根据接口返回结果得到数据数组 let arr = data.data; callBack(arr); }) . catch ((error) => { console.log(error); }) } // 20180730 子组件渲染 _renderRow(row) { return ( <CommentItem row={row} /> ) } |
3.效果图
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步