關(guān)于React Native的下拉刷新,雖然官方出了一個(gè)控件RefreshControl
,但可定制性太差,基本上樣式固定了.最近將公司的Android端用RN重寫(xiě)一下,之前用的是PullToRefresh
,于是準(zhǔn)備用React Native
寫(xiě)一套該效果出來(lái).
效果圖:
與
PullToRefresh
類(lèi)似,不光是ListView
控件可以下拉刷新,任一控件都可以.實(shí)現(xiàn)原理
PanResponder
在React Native的API中有一個(gè)PanResponder
,它能檢測(cè)到用戶(hù)的手勢(shì),與Android中的 事件分發(fā)機(jī)制
的作用類(lèi)似.可以捕獲到用戶(hù)的touch down
,touch move
,touch up
事件. 然后根據(jù)用戶(hù)按下的距離,對(duì)整個(gè)View
進(jìn)行y軸的移動(dòng).
setNativeProps
在捕獲到用戶(hù)的下拉軌跡后,需要 子控件 跟隨用戶(hù)手勢(shì),這里可以改變子控件的marginTop
或者translateY
的值.
這里比較適合用setNativeProps
來(lái)直接改變值,如果用state
狀態(tài)機(jī)來(lái)動(dòng)態(tài)更改,會(huì)造成 View的多次重復(fù)render
,造成不必要的性能損耗.
LayoutAnimation
當(dāng)用戶(hù)刷新動(dòng)作完成之后,需要程序自動(dòng)將 正在刷新的布局恢復(fù)原狀,這里使用LayoutAnimation
可以很簡(jiǎn)單的做到.
shouldComponentUpdate
該函數(shù) 可用來(lái)比較哪些狀態(tài)的更改需要重新render
,用當(dāng)前的state
與將要改變的state
比較是否一致.
完整源碼:
'use strict'
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View,
PanResponder,
LayoutAnimation,
ProgressBarAndroid,
Dimensions,
Text,
AsyncStorage,
Image
} from 'react-native';
let self;
/**ref的引用*/
const PULL_REFRESH_LAYOUT="pullLayout";
/**屏幕寬度*/
const deviceWidth = Dimensions.get('window').width;
/**下拉阻力系數(shù)*/
const factor=1.8;
/**最大下拉高度*/
const MAX_PULL_LENGTH=170;
/**Loading的高度*/
const REFRESH_PULL_LENGTH=70;
/**動(dòng)畫(huà)時(shí)長(zhǎng)*/
const BACK_TIME=400;
/**存儲(chǔ)最后刷新時(shí)間的Key*/
const REFRESH_LAST_TIME_KEY="refresh_last";
const RefreshStatus={
Refresh_NONE:0,
Refresh_Drag_Down:1,
Refresh_Loading:2,
Refresh_Reset:3,
};
const ShowLoadingStatus={
SHOW_DOWN:0,
SHOW_UP:1,
SHOW_LOADING:2,
};
class PullToRefreshLayout extends Component{
_panResponder:{}
// 構(gòu)造
constructor(props) {
super(props);
// 初始狀態(tài)
this.state = {
currentDistance:0,
pullRefreshStatus:RefreshStatus.Refresh_NONE,
showPullStatus:ShowLoadingStatus.SHOW_DOWN,
showPullLastTime:'NONE',
};
this.resetHeader=this.resetHeader.bind(this);
this.refreshStateHeader=this.refreshStateHeader.bind(this);
this.getTime=this.getTime.bind(this);
this.addZeroAtFront=this.addZeroAtFront.bind(this);
}
//要求成為響應(yīng)者
_handleStartShouldSetPanResponder(e: Object, gestureState: Object): boolean {
return true;
}
_handleMoveShouldSetPanResponder(e: Object, gestureState: Object): boolean {
return true;
}
//touch down 開(kāi)始手勢(shì)操作支竹。給用戶(hù)一些視覺(jué)反饋蚁阳,讓他們知道發(fā)生了什么事情体斩!
_handlePanResponderGrant(e: Object, gestureState: Object){
}
//touch move 響應(yīng)滑動(dòng)事件
_handlePanResponderMove(e: Object, gestureState: Object) {
if(self.state.currentDistance>REFRESH_PULL_LENGTH){
if(self.state.showPullStatus===ShowLoadingStatus.SHOW_DOWN){
self.setState({
showPullStatus:ShowLoadingStatus.SHOW_UP,
});
}
}
else{
if (self.state.showPullStatus===ShowLoadingStatus.SHOW_UP){
self.setState({
showPullStatus:ShowLoadingStatus.SHOW_DOWN,
});
}
}
if (self.state.pullRefreshStatus===RefreshStatus.Refresh_Loading){
self.setState({
currentDistance:REFRESH_PULL_LENGTH+gestureState.dy/factor,
// refreshStateHeader:2,
});
self.refs[PULL_REFRESH_LAYOUT].setNativeProps({
style:{
marginTop:self.state.currentDistance,
}
});
return;
}
if (gestureState.dy>0&&self.state.currentDistance<MAX_PULL_LENGTH){
self.setState({
currentDistance:gestureState.dy/factor,
pullRefreshStatus:RefreshStatus.Refresh_Drag_Down,
});
self.refs[PULL_REFRESH_LAYOUT].setNativeProps({
style:{
marginTop:self.state.currentDistance,
}
});
}
else if(gestureState.dy>0&&self.state.currentDistance>MAX_PULL_LENGTH){//則不再往下移動(dòng)
self.setState({
currentDistance:MAX_PULL_LENGTH,
pullRefreshStatus:RefreshStatus.Refresh_Drag_Down,
});
self.refs[PULL_REFRESH_LAYOUT].setNativeProps({
style:{
marginTop:self.state.currentDistance,
}
});
}
}
resetHeader(){
LayoutAnimation.configureNext({
duration: BACK_TIME,
update: {
type: 'linear',
}
});
self.refs[PULL_REFRESH_LAYOUT].setNativeProps({
style:{
marginTop:0,
}
});
self.setState({
currentDistance:0,
pullRefreshStatus:RefreshStatus.Refresh_Reset,
showPullStatus:ShowLoadingStatus.SHOW_DOWN,
});
}
refreshStateHeader(){
self.setState({
pullRefreshStatus:RefreshStatus.Refresh_Loading,
currentDistance:REFRESH_PULL_LENGTH,
showPullStatus:ShowLoadingStatus.SHOW_LOADING,
},()=>{
if(self.props.onRefresh){
self.props.onRefresh();
}
});
LayoutAnimation.configureNext({
duration: BACK_TIME,
update: {
type: 'linear',
}
});
self.refs[PULL_REFRESH_LAYOUT].setNativeProps({
style:{
marginTop:REFRESH_PULL_LENGTH,
}
});
}
addZeroAtFront(count){
if (count<10){
count="0"+count;
}
return count;
}
getTime(){
let date=new Date();
let mMonth=this.addZeroAtFront(date.getMonth()+1);
let mDate=this.addZeroAtFront(date.getDate());
let mHours=this.addZeroAtFront(date.getHours());
let mMinutes=this.addZeroAtFront(date.getMinutes());
return mMonth+"-"+mDate+" "+mHours+":"+mMinutes;
}
stopRefresh(){
let savedDate=this.getTime();
self.setState({
showPullLastTime:savedDate,
});
AsyncStorage.setItem(REFRESH_LAST_TIME_KEY,savedDate,()=>{
});
this.resetHeader();
}
_handlePanResponderEnd(e: Object, gestureState: Object) {
if (self.state.currentDistance>=REFRESH_PULL_LENGTH){
self.refreshStateHeader();
}
else{
self.resetHeader();
}
}
componentDidMount() {
AsyncStorage.getItem(REFRESH_LAST_TIME_KEY,(err,result)=>{
if (result){
self.setState({
showPullLastTime:result,
});
}
});
}
componentWillMount() {
self=this;
this._panResponder=PanResponder.create({
onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminate: this._handlePanResponderEnd,
});
}
shouldComponentUpdate(nextProps,nextState) {
if (nextState.showPullStatus!==self.state.showPullStatus){
return true;
}
if (self.state.showPullLastTime!==nextState.showPullLastTime){
return true;
}
return false;
}
render(){
let pullText;
let indicatorView;
if (this.state.showPullStatus===ShowLoadingStatus.SHOW_DOWN){
indicatorView=<Image
style={{height:30,width:30,marginRight:10}}
source={require('./img/ptr_rotate_arrow.png')}
resizeMode={Image.resizeMode.contain}
/>;
pullText="下拉刷新";
}
else if (this.state.showPullStatus===ShowLoadingStatus.SHOW_UP){
indicatorView=<Image
style={{height:30,width:30,marginRight:10,transform:[{rotate:"180deg"}]}}
source={require('./img/ptr_rotate_arrow.png')}
resizeMode={Image.resizeMode.contain}
/>;
pullText="釋放刷新";
}
else if(this.state.showPullStatus===ShowLoadingStatus.SHOW_LOADING){
indicatorView=<ProgressBarAndroid style={{marginRight:10,width:30,height:30}} />
pullText="刷新中......";
}
return (
<View style={styles.base}>
<View style={{backgroundColor:'white',position:'absolute',}}>
<View style={{justifyContent:'center',alignItems:'center',width:deviceWidth,height:REFRESH_PULL_LENGTH,flexDirection:'row'}}>
{indicatorView}
<View style={{height:REFRESH_PULL_LENGTH,justifyContent:'center',alignItems:'center',marginLeft:10}}>
<Text style={{fontSize:12,color:'#666',marginBottom:1}}>{pullText}</Text>
<Text style={{fontSize:12,color:'#666',marginTop:1}}>最后更新: {this.state.showPullLastTime}</Text>
</View>
</View>
</View>
<View
ref={PULL_REFRESH_LAYOUT}
style={{flex:1,position:'absolute'}} {...this._panResponder.panHandlers} >
{this.props.children}
</View>
</View>
);
}
}
export default PullToRefreshLayout;
var styles = StyleSheet.create({
base: {
flex: 1,
position :'relative'
},
});
使用代碼