您的当前位置:首页正文

react-native ListView 封装 实现 下拉刷新/上拉加载更多

2024-12-02 来源:个人技术集锦

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&&notLast);
      }
    }
    // let ifRenderItemS=this.props.ItemSeparatorComponent&&((this.props.pageLen&&(this.state.canLoad||notLast))||(!this.props.pageLen&&notLast));
    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.效果图

显示全文