1.PageListView 組件封裝
src/components/PageListView/index.js
/**
* 上拉刷新/下拉加載更多 組件
*/
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.頁面調用
<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.效果圖


