From ac5cf546ef664c9e7c2ccd4f3528a0b07bc76a7c Mon Sep 17 00:00:00 2001 From: deveix Date: Mon, 29 Mar 2021 07:02:29 +0200 Subject: [PATCH] Changing old react component to use react hooks --- README.md | 147 +++-- .../weatherAnimation/index.android.js | 323 +++++------ src/AnimatedPullToRefresh.js | 529 ++++++++---------- 3 files changed, 460 insertions(+), 539 deletions(-) diff --git a/README.md b/README.md index 58a4840..9259064 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,24 @@ -### Custom Android Pull to Refresh +### Custom Pull to Refresh Component Inspired by the shots from the author: https://dribbble.com/yupnguyen [Expo Demo](https://expo.io/@devilsanek/animated-pull-to-refresh) -| Coffee Concept | Coin Concept | Weather Concept -| ------------------------- |:-----------------------:|:-----------------------:| -| ![Output sample](https://github.com/NadiKuts/react-native-pull-down/blob/master/examples/SimpleAnimations/resources/coffee_animation.gif)|![Output sample](https://github.com/NadiKuts/react-native-pull-down/blob/master/examples/SimpleAnimations/resources/coin_animation.gif) |![Output sample](https://github.com/NadiKuts/react-native-pull-down/blob/master/examples/SimpleAnimations/resources/weather_animation.gif)| +| Coffee Concept | Coin Concept | Weather Concept | +| ----------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------: | +| ![Output sample](https://github.com/NadiKuts/react-native-pull-down/blob/master/examples/SimpleAnimations/resources/coffee_animation.gif) | ![Output sample](https://github.com/NadiKuts/react-native-pull-down/blob/master/examples/SimpleAnimations/resources/coin_animation.gif) | ![Output sample](https://github.com/NadiKuts/react-native-pull-down/blob/master/examples/SimpleAnimations/resources/weather_animation.gif) | ### Description -Currently, react-native provides RefreshControl out of the box. (Which uses standard circle android animation). -https://facebook.github.io/react-native/docs/refreshcontrol.html. +Currently, react-native provides RefreshControl out of the box. (Which uses standard circle android animation). +https://facebook.github.io/react-native/docs/refreshcontrol.html. -However, it is not 'yet' possible to override the animation that runs during refreshing phase. This package aims to fill this gap and provide a 'relatively' easy way to add your own custom animation. +However, it is not 'yet' possible to override the animation that runs during refreshing phase. This package aims to fill this gap and provide a 'relatively' easy way to add your own custom animation. ### Installation -1. Install the package using either: +1. Install the package using either: + ```sh $ npm install --save react-native-pull-refresh # or @@ -25,7 +26,7 @@ $ yarn add react-native-pull-refresh ``` 2. Install and link the Lottie package (renders Adobe After Effect animations): -https://github.com/airbnb/lottie-react-native + https://github.com/airbnb/lottie-react-native ```sh yarn add lottie-react-native @@ -45,59 +46,58 @@ You can find `< Header />` and `< ScrollItem />` components in the sample folder import PullToRefresh from 'react-native-pull-refresh'; export default class weatherAnimation extends Component { - constructor( ) { - super( ); - this.state = { - isRefreshing: false, - }; - } - - onRefresh() { - this.setState({isRefreshing: true}); - - // Simulate fetching data from the server - setTimeout(() => { - this.setState({isRefreshing: false}); - }, 5000); - } - - render() { - return ( - -
- - - - - - - - - - - - - } - - onPullAnimationSrc ={require('./umbrella_pull.json')} - onStartRefreshAnimationSrc ={require('./umbrella_start.json')} - onRefreshAnimationSrc = {require('./umbrella_repeat.json')} - onEndRefreshAnimationSrc = {require('./umbrella_end.json')} - /> - - - ); - } + constructor() { + super(); + this.state = { + isRefreshing: false, + }; + } + + onRefresh() { + this.setState({ isRefreshing: true }); + + // Simulate fetching data from the server + setTimeout(() => { + this.setState({ isRefreshing: false }); + }, 5000); + } + + render() { + return ( + +
+ + + // content to pull goes here + + + + + + + + + + + + + + + ); + } } ``` #### Animation Files Format + Lottie JSON - https://github.com/airbnb/lottie-react-native Lottie is a mobile library, developed by AirBnB for Android and iOS that parses Adobe After Effects animations exported as JSON with bodymovin and renders them natively on mobile. @@ -108,36 +108,35 @@ You can find file examples in `examples/SimpleAnimations/animations` folder #### General Props -| Prop | Type | Description | -|---|---|---| -|**`isRefreshing`**|`Boolean`|Refresh state set by parent to trigger refresh.| -|**`pullHeight`**|`Integer`|Pull Distance _Default 180._| -|**`onRefresh`**|`Function`|Callback after refresh event| -|**`contentView`**|`Object`|The content: ScrollView or ListView| -|**`animationBackgroundColor`**|`string`|Background color| -|**`onScroll`**|`Function`|Custom onScroll event| +| Prop | Type | Description | +| ------------------------------ | ---------- | ----------------------------------------------- | +| **`isRefreshing`** | `Boolean` | Refresh state set by parent to trigger refresh. | +| **`pullHeight`** | `Integer` | Pull Distance _Default 180._ | +| **`onRefresh`** | `Function` | Callback after refresh event | +| **`animationBackgroundColor`** | `string` | Background color | +| **`onScroll`** | `Function` | Custom onScroll event | #### Animation Source Files Props -| Prop | Description | -|---|---| -|**`onPullAnimationSrc`**|Animation JSON that runs when scroll view is pulled down| -|**`onStartRefreshAnimationSrc`**|Animation JSON that runs after view was pulled and released| -|**`onRefreshAnimationSrc`**|Animation JSON that runs continuously until isRefreshing props is not changed| -|**`onEndRefreshAnimationSrc`**|Animation JSON that runs after isRefreshing props is changed| +| Prop | Description | +| -------------------------------- | ----------------------------------------------------------------------------- | +| **`onPullAnimationSrc`** | Animation JSON that runs when scroll view is pulled down | +| **`onStartRefreshAnimationSrc`** | Animation JSON that runs after view was pulled and released | +| **`onRefreshAnimationSrc`** | Animation JSON that runs continuously until isRefreshing props is not changed | +| **`onEndRefreshAnimationSrc`** | Animation JSON that runs after isRefreshing props is changed | ### Demo + The demo app can be found at `examples/SimpleAnimations`. Install Expo App on your [Android smartphone](https://play.google.com/store/apps/details?id=host.exp.exponent&referrer=www) -Scan this QR-code with your Expo App. +Scan this QR-code with your Expo App. ![alt text](https://github.com/NadiKuts/react-native-pull-refresh/blob/master/examples/SimpleAnimations/resources/scan.png) ... or go [here](https://expo.io/@devilsanek/animated-pull-to-refresh) and try it out! - ### Contribution / Issues Are very welcome! :) diff --git a/examples/SimpleAnimations/weatherAnimation/index.android.js b/examples/SimpleAnimations/weatherAnimation/index.android.js index 0182b54..a6a5728 100644 --- a/examples/SimpleAnimations/weatherAnimation/index.android.js +++ b/examples/SimpleAnimations/weatherAnimation/index.android.js @@ -1,20 +1,5 @@ import React, { Component } from 'react'; -import { - AppRegistry, - Dimensions, - PanResponder, - View, - Animated, - ListView, - RefreshControl, - Text, - Progress, - StyleSheet, - ScrollView, - UIManager, - StatusBar -} from 'react-native'; - +import { AppRegistry, Dimensions, View, ScrollView } from 'react-native'; import PullToRefresh from 'react-native-pull-refresh'; @@ -22,168 +7,162 @@ const HEIGHT = Dimensions.get('window').height; const WIDTH = Dimensions.get('window').width; class Header extends Component { - constructor(props){ - super(props) - this.state = { - width: 0, - height: 0 - } - this.measureView = this.measureView.bind(this); - } - - measureView(event) { - this.setState({ - width: event.nativeEvent.layout.width, - height: event.nativeEvent.layout.height - }) - } - - render(){ - const mainStyle = { - flex: 1, - backgroundColor: '#F8F4FC', - justifyContent: 'center', - alignItems: 'center', - borderBottomWidth: 1, - borderBottomColor: '#8B8393' - } - - const submenuStyle = { - width: this.state.width / 2, - height: this.state.height / 4, - borderRadius: 50, - backgroundColor: '#8B8393' - } - return ( - this.measureView(event)}> - - - ) - } + constructor(props) { + super(props); + this.state = { + width: 0, + height: 0, + }; + this.measureView = this.measureView.bind(this); + } + + measureView(event) { + this.setState({ + width: event.nativeEvent.layout.width, + height: event.nativeEvent.layout.height, + }); + } + + render() { + const mainStyle = { + flex: 1, + backgroundColor: '#F8F4FC', + justifyContent: 'center', + alignItems: 'center', + borderBottomWidth: 1, + borderBottomColor: '#8B8393', + }; + + const submenuStyle = { + width: this.state.width / 2, + height: this.state.height / 4, + borderRadius: 50, + backgroundColor: '#8B8393', + }; + return ( + this.measureView(event)}> + + + ); + } } class ScrollItem extends Component { - constructor(props){ - super(props) - this.state = { - height: 100 - } - } - - render(){ - const mainStyle = { - flex: 1, - height: this.state.height, - backgroundColor: '#DCDADF', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - borderBottomWidth: 1, - borderBottomColor: '#8B8393' - } - const imgContainer = { - flex: 1, - justifyContent: 'center', - alignItems: 'center' - } - - const imgStyle = { - width: this.state.height / 1.5, - height: this.state.height / 1.5, - backgroundColor: '#ADA8B3', - borderRadius: 10 - } - - const textContainer = { - flex: 3, - height: this.state.height / 1.5, - flexDirection: 'column', - justifyContent: 'flex-start' - } - - const textStyle = { - width: WIDTH / 1.8, - marginBottom: 10, - height: this.state.height / 8, - backgroundColor: '#ADA8B3', - borderRadius: 10 - } - - const textStyleShort = { - width: WIDTH / 3, - marginBottom: 10, - height: this.state.height / 9, - backgroundColor: '#ADA8B3', - borderRadius: 12 - } - - return ( - - - - - - - - - - - - - ) - } + constructor(props) { + super(props); + this.state = { + height: 100, + }; + } + + render() { + const mainStyle = { + flex: 1, + height: this.state.height, + backgroundColor: '#DCDADF', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + borderBottomWidth: 1, + borderBottomColor: '#8B8393', + }; + const imgContainer = { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }; + + const imgStyle = { + width: this.state.height / 1.5, + height: this.state.height / 1.5, + backgroundColor: '#ADA8B3', + borderRadius: 10, + }; + + const textContainer = { + flex: 3, + height: this.state.height / 1.5, + flexDirection: 'column', + justifyContent: 'flex-start', + }; + + const textStyle = { + width: WIDTH / 1.8, + marginBottom: 10, + height: this.state.height / 8, + backgroundColor: '#ADA8B3', + borderRadius: 10, + }; + + const textStyleShort = { + width: WIDTH / 3, + marginBottom: 10, + height: this.state.height / 9, + backgroundColor: '#ADA8B3', + borderRadius: 12, + }; + + return ( + + + + + + + + + + + + ); + } } - export default class weatherAnimation extends Component { - constructor(props) { - super(props); - this.state = { - isRefreshing: false, - }; - } - - onRefresh() { - this.setState({isRefreshing: true}); - setTimeout(() => { - this.setState({isRefreshing: false}); - }, 5000); - } - - render() { - return ( - -
- - - - - - - - - - - - - } - - onPullAnimationSrc ={require('./umbrella_1.json')} - onStartRefreshAnimationSrc ={require('./umbrella_start.json')} - onRefreshAnimationSrc = {require('./umbrella_repeat.json')} - onEndRefreshAnimationSrc = {require('./umbrella_end.json')} - /> - - - ); - } + constructor(props) { + super(props); + this.state = { + isRefreshing: false, + }; + } + + onRefresh() { + this.setState({ isRefreshing: true }); + setTimeout(() => { + this.setState({ isRefreshing: false }); + }, 5000); + } + + render() { + return ( + +
+ + + + + + + + + + + + + + + + + ); + } } - AppRegistry.registerComponent('weatherAnimation', () => weatherAnimation); diff --git a/src/AnimatedPullToRefresh.js b/src/AnimatedPullToRefresh.js index 7f364c8..2bb9534 100644 --- a/src/AnimatedPullToRefresh.js +++ b/src/AnimatedPullToRefresh.js @@ -1,296 +1,239 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - View, - ScrollView, - Animated, - PanResponder, - UIManager, - Dimensions, -} from 'react-native'; +import React, { useState, useEffect, useRef } from 'react'; +import { View, ScrollView, Animated, PanResponder, UIManager, Dimensions } from 'react-native'; import Animation from 'lottie-react-native'; -class AnimatedPullToRefresh extends React.Component { - constructor(props) { - super(props); - this.state = { - scrollY: new Animated.Value(0), - refreshHeight: new Animated.Value(0), - currentY: 0, - isScrollFree: false, - - isRefreshAnimationStarted: false, - isRefreshAnimationEnded: false, - initAnimationProgress: new Animated.Value(0), - repeatAnimationProgress: new Animated.Value(0), - finalAnimationProgress: new Animated.Value(0), - }; - - this.onRepeatAnimation = this.onRepeatAnimation.bind(this); - this.onEndAnimation = this.onEndAnimation.bind(this); - - UIManager.setLayoutAnimationEnabledExperimental && - UIManager.setLayoutAnimationEnabledExperimental(true); - } - - static propTypes = { - /** - * Refresh state set by parent to trigger refresh - * @type {Boolean} - */ - isRefreshing: PropTypes?.bool.isRequired, - /** - * Pull Distance - * @type {Integer} - */ - pullHeight: PropTypes?.number, - /** - * Callback after refresh event - * @type {Function} - */ - onRefresh: PropTypes?.func.isRequired, - /** - * The content: ScrollView or ListView - * @type {Object} - */ - contentView: PropTypes?.object.isRequired, - /** - * Background color - * @type {string} - */ - animationBackgroundColor: PropTypes?.string, - /** - * Custom onScroll event - * @type {Function} - */ - onScroll: PropTypes.func, - }; - - static defaultProps = { - pullHeight: 180, - animationBackgroundColor: 'white', - }; - - componentWillMount() { - this._panResponder = PanResponder.create({ - onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder.bind( - this, - ), - onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder.bind( - this, - ), - onPanResponderMove: this._handlePanResponderMove.bind(this), - onPanResponderRelease: this._handlePanResponderEnd.bind(this), - onPanResponderTerminate: this._handlePanResponderEnd.bind(this), - }); - } - - componentWillReceiveProps(props) { - if (this.props.isRefreshing !== props.isRefreshing) { - // Finish the animation and set refresh panel height to 0 - if (!props.isRefreshing) { - } - } - } - - _handleStartShouldSetPanResponder(e, gestureState) { - return !this.state.isScrollFree; - } - - _handleMoveShouldSetPanResponder(e, gestureState) { - return !this.state.isScrollFree; - } - - // if the content scroll value is at 0, we allow for a pull to refresh - _handlePanResponderMove(e, gestureState) { - if (!this.props.isRefreshing) { - if ( - (gestureState.dy >= 0 && this.state.scrollY._value === 0) || - this.state.refreshHeight._value > 0 - ) { - this.state.refreshHeight.setValue(-1 * gestureState.dy * 0.5); - } else { - // Native android scrolling - this.refs.scrollComponentRef.scrollTo({ - y: -1 * gestureState.dy, - animated: true, - }); - } - } - } - - _handlePanResponderEnd(e, gestureState) { - if (!this.props.isRefreshing) { - if (this.state.refreshHeight._value <= -this.props.pullHeight) { - this.onScrollRelease(); - Animated.parallel([ - Animated.spring(this.state.refreshHeight, { - toValue: -this.props.pullHeight, - }), - Animated.timing(this.state.initAnimationProgress, { - toValue: 1, - duration: 1000, - }), - ]).start(() => { - this.state.initAnimationProgress.setValue(0); - this.setState({isRefreshAnimationStarted: true}); - this.onRepeatAnimation(); - }); - } else if (this.state.refreshHeight._value <= 0) { - Animated.spring(this.state.refreshHeight, { - toValue: 0, - }).start(); - } - - if (this.state.scrollY._value > 0) { - this.setState({isScrollFree: true}); - } - } - } - - onRepeatAnimation() { - this.state.repeatAnimationProgress.setValue(0); - - Animated.timing(this.state.repeatAnimationProgress, { - toValue: 1, - duration: 1000, - }).start(() => { - if (this.props.isRefreshing) { - this.onRepeatAnimation(); - } else { - this.state.repeatAnimationProgress.setValue(0); - this.onEndAnimation(); - } - }); - } - - onEndAnimation() { - this.setState({isRefreshAnimationEnded: true}); - Animated.sequence([ - Animated.timing(this.state.finalAnimationProgress, { - toValue: 1, - duration: 1000, - }), - Animated.spring(this.state.refreshHeight, { - toValue: 0, - bounciness: 12, - }), - ]).start(() => { - this.state.finalAnimationProgress.setValue(0); - this.setState({ - isRefreshAnimationEnded: false, - isRefreshAnimationStarted: false, - }); - }); - } - - - onScrollRelease() { - if (!this.props.isRefreshing) { - this.props.onRefresh(); - } - } - - isScrolledToTop() { - if (this.state.scrollY._value === 0 && this.state.isScrollFree) { - this.setState({isScrollFree: false}); - } - } - - render() { - const onScrollEvent = event => { - this.state.scrollY.setValue(event.nativeEvent.contentOffset.y); - }; - - const animateHeight = this.state.refreshHeight.interpolate({ - inputRange: [-this.props.pullHeight, 0], - outputRange: [this.props.pullHeight, 0], - }); - - const animateProgress = this.state.refreshHeight.interpolate({ - inputRange: [-this.props.pullHeight, 0], - outputRange: [1, 0], - extrapolate: 'clamp', - }); - - const animationStyle = { - position: 'absolute', - top: 0, - bottom: 0, - right: 0, - left: 0, - backgroundColor: this.props.animationBackgroundColor, - width: Dimensions.get('window').width, - height: this.props.pullHeight, - }; - - return ( - - - - - - - { - this.isScrolledToTop(); - }} - onScrollEndDrag={() => { - this.isScrolledToTop(); - }}> - - {React.cloneElement(this.props.contentView, { - scrollEnabled: false, - ref: 'scrollComponentRef', - })} - - - - ); - } -} - -module.exports = AnimatedPullToRefresh; +const AnimatedPullToRefresh = ({ + isRefreshing, + onRefresh, + pullHeight = 180, + animationBackgroundColor = 'white', + onPullAnimationSrc, + onStartRefreshAnimationSrc, + onRefreshAnimationSrc, + onEndRefreshAnimationSrc, + children, +}) => { + const [_state, _setState] = useState({ + currentY: 0, + isScrollFree: false, + isRefreshAnimationStarted: false, + isRefreshAnimationEnded: false, + }); + const [_animationCount, _setAnimationCount] = useState(0); + const scrollY = useRef(new Animated.Value(0)).current; + const refreshHeight = useRef(new Animated.Value(0)).current; + const initAnimationProgress = useRef(new Animated.Value(0)).current; + const repeatAnimationProgress = useRef(new Animated.Value(0)).current; + const finalAnimationProgress = useRef(new Animated.Value(0)).current; + + const scrollComponentRef = useRef(null); + const _panResponder = useRef(null); + + useEffect(() => { + _panResponder.current = PanResponder.create({ + onStartShouldSetPanResponder: _handleStartShouldSetPanResponder, + onMoveShouldSetPanResponder: _handleMoveShouldSetPanResponder, + onPanResponderMove: _handlePanResponderMove, + onPanResponderRelease: _handlePanResponderEnd, + onPanResponderTerminate: _handlePanResponderEnd, + }); + UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); + }, []); + + const _handleStartShouldSetPanResponder = (e, gestureState) => { + return !_state.isScrollFree; + }; + + const _handleMoveShouldSetPanResponder = (e, gestureState) => { + return !_state.isScrollFree; + }; + + //if the content scroll value is at 0, we allow for a pull to refresh + const _handlePanResponderMove = (e, gestureState) => { + if (!isRefreshing) { + if ((gestureState.dy >= 0 && scrollY?._value === 0) || refreshHeight?._value > 0) { + refreshHeight?.setValue(-1 * gestureState.dy * 0.5); + } else { + // Native android scrolling + scrollComponentRef?.current?.scrollTo({ + y: -1 * gestureState.dy, + animated: true, + }); + } + } + }; + + const _handlePanResponderEnd = (e, gestureState) => { + if (!isRefreshing) { + if (refreshHeight?._value <= -pullHeight) { + onScrollRelease(); + Animated.parallel([ + Animated.spring(refreshHeight, { + toValue: -pullHeight, + useNativeDriver: false, + }), + Animated.timing(initAnimationProgress, { + useNativeDriver: false, + toValue: 1, + duration: 1000, + }), + ]).start(() => { + initAnimationProgress?.setValue(0); + _setState({ + ..._state, + isRefreshAnimationStarted: true, + isRefreshAnimationEnded: false, + }); + onRepeatAnimation(); + }); + } else if (refreshHeight?._value <= 0) { + Animated.spring(refreshHeight, { + toValue: 0, + useNativeDriver: false, + }).start(); + } + + if (scrollY?._value > 0) { + _setState({ ..._state, isScrollFree: true }); + } + } + }; + useEffect(() => { + if (!_state.isRefreshAnimationStarted && isRefreshing) { + refreshHeight.setValue(-pullHeight); + repeatAnimationProgress?.setValue(1); + _setState({ + ..._state, + isRefreshAnimationStarted: true, + isRefreshAnimationEnded: false, + }); + onRepeatAnimation(); + } + }, [_state.isRefreshAnimationStarted, isRefreshing]); + useEffect(() => onRepeatAnimation(), [_animationCount]); + + const onRepeatAnimation = () => { + repeatAnimationProgress?.setValue(0); + Animated.timing(repeatAnimationProgress, { + useNativeDriver: false, + toValue: 1, + duration: 1000, + }).start(() => { + if (isRefreshing) { + _setAnimationCount((cc) => cc + 1); + } else { + repeatAnimationProgress?.setValue(0); + onEndAnimation(); + } + }); + }; + + const onEndAnimation = () => { + _setState({ ..._state, isRefreshAnimationEnded: true }); + Animated.sequence([ + Animated.timing(finalAnimationProgress, { + useNativeDriver: false, + toValue: 1, + duration: 1000, + }), + Animated.spring(refreshHeight, { + useNativeDriver: false, + toValue: 0, + bounciness: 12, + }), + ]).start(() => { + finalAnimationProgress?.setValue(0); + _setState({ + ..._state, + isRefreshAnimationEnded: false, + isRefreshAnimationStarted: false, + }); + }); + }; + + const onScrollRelease = () => { + if (!isRefreshing) { + onRefresh(); + } + }; + + const isScrolledToTop = () => { + if (scrollY?._value === 0 && _state.isScrollFree) { + _setState({ ..._state, isScrollFree: false }); + } + }; + let onScrollEvent = (event) => { + scrollY?.setValue(event.nativeEvent.contentOffset.y); + }; + + let animateHeight = refreshHeight?.interpolate({ + inputRange: [-pullHeight, 0], + outputRange: [pullHeight, 0], + }); + + let animateProgress = refreshHeight?.interpolate({ + inputRange: [-pullHeight, 0], + outputRange: [1, 0], + extrapolate: 'clamp', + }); + + const animationStyle = { + position: 'absolute', + top: 0, + bottom: 0, + right: 0, + left: 0, + backgroundColor: animationBackgroundColor, + width: Dimensions.get('window').width, + height: pullHeight, + }; + + return ( + + + + + + + { + isScrolledToTop(); + }} + onScrollEndDrag={() => { + isScrolledToTop(); + }}> + + {React.cloneElement(children, { + scrollEnabled: false, + ref: scrollComponentRef, + })} + + + + ); +}; + +export default AnimatedPullToRefresh;