Skip to content

Commit

Permalink
Cross platform pull to refresh component.
Browse files Browse the repository at this point in the history
When put inside a ScrollView it will add a UIRefreshControl on iOS and a
SwipeRefreshLayout on Android. Adds support for tintColor and title on iOS
and all the props currently supported on PullToRefreshViewAndroid on
Android.

This will allow to deprecate/remove the onRefreshStart prop on ScrollView
and PullToRefreshViewAndroid in a future PR.
  • Loading branch information
janicduplessis committed Jan 1, 2016
1 parent 718cd79 commit c632f4d
Show file tree
Hide file tree
Showing 13 changed files with 418 additions and 8 deletions.
122 changes: 122 additions & 0 deletions Examples/UIExplorer/RefreshControlExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
'use strict';

const React = require('react-native');
const {
ScrollView,
StyleSheet,
RefreshControl,
Text,
TouchableWithoutFeedback,
View,
} = React;

const styles = StyleSheet.create({
row: {
borderColor: 'grey',
borderWidth: 1,
padding: 20,
backgroundColor: '#3a5795',
margin: 5,
},
text: {
alignSelf: 'center',
color: '#fff',
},
scrollview: {
flex: 1,
},
});

const Row = React.createClass({
_onClick: function() {
this.props.onClick(this.props.data);
},
render: function() {
return (
<TouchableWithoutFeedback onPress={this._onClick} >
<View style={styles.row}>
<Text style={styles.text}>
{this.props.data.text + ' (' + this.props.data.clicks + ' clicks)'}
</Text>
</View>
</TouchableWithoutFeedback>
);
},
});

const RefreshControlExample = React.createClass({
statics: {
title: '<RefreshControl>',
description: 'Adds pull-to-refresh support to a scrollview.'
},

getInitialState() {
return {
isRefreshing: false,
loaded: 0,
rowData: Array.from(new Array(20)).map(
(val, i) => ({text: 'Initial row' + i, clicks: 0})),
};
},

_onClick(row) {
row.clicks++;
this.setState({
rowData: this.state.rowData,
});
},

render() {
const rows = this.state.rowData.map((row, ii) => {
return <Row key={ii} data={row} onClick={this._onClick}/>;
});
return (
<ScrollView style={styles.scrollview}>
<RefreshControl
refreshing={this.state.isRefreshing}
onRefresh={this._onRefresh}
tintColor="#ff0000"
title="Loading..."
colors={['#ff0000', '#00ff00', '#0000ff']}
progressBackgroundColor={'#ffff00'}
/>
{rows}
</ScrollView>
);
},

_onRefresh() {
this.setState({isRefreshing: true});
setTimeout(() => {
// prepend 10 items
const rowData = Array.from(new Array(10))
.map((val, i) => ({
text: 'Loaded row' + (+this.state.loaded + i),
clicks: 0,
}))
.concat(this.state.rowData);

this.setState({
loaded: this.state.loaded + 10,
isRefreshing: false,
rowData: rowData,
});
}, 5000);
},
});

module.exports = RefreshControlExample;
1 change: 1 addition & 0 deletions Examples/UIExplorer/UIExplorerList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var COMPONENTS = [
require('./ProgressBarAndroidExample'),
require('./ScrollViewSimpleExample'),
require('./SwitchAndroidExample'),
require('./RefreshControlExample'),
require('./PullToRefreshViewAndroidExample.android'),
require('./TextExample.android'),
require('./TextInputExample.android'),
Expand Down
1 change: 1 addition & 0 deletions Examples/UIExplorer/UIExplorerList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var COMPONENTS = [
require('./NavigatorIOSExample'),
require('./PickerIOSExample'),
require('./ProgressViewIOSExample'),
require('./RefreshControlExample'),
require('./ScrollViewExample'),
require('./SegmentedControlIOSExample'),
require('./SliderIOSExample'),
Expand Down
109 changes: 109 additions & 0 deletions Libraries/Components/RefreshControl/RefreshControl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule RefreshControl
*/
'use strict';

const React = require('React');
const Platform = require('Platform');

const requireNativeComponent = require('requireNativeComponent');

if (Platform.OS === 'ios') {
var RefreshLayoutConsts = {SIZE: {}};
} else if (Platform.OS === 'android') {
var RefreshLayoutConsts = require('NativeModules').UIManager.AndroidSwipeRefreshLayout.Constants;
}

/**
* This component is used inside a ScrollView to add pull to refresh
* functionality. When the ScrollView is at `scrollY: 0`, swiping down
* triggers an `onRefresh` event.
*/
const RefreshControl = React.createClass({
statics: {
SIZE: RefreshLayoutConsts.SIZE,
},

propTypes: {
/**
* Called when the view starts refreshing.
*/
onRefresh: React.PropTypes.func,
/**
* Whether the view should be indicating an active refresh.
*/
refreshing: React.PropTypes.bool,
/**
* The color of the refresh indicator.
* @platform ios
*/
tintColor: React.PropTypes.string,
/**
* The title displayed under the refresh indicator.
* @platform ios
*/
title: React.PropTypes.string,
/**
* Whether the pull to refresh functionality is enabled.
* @platform android
*/
enabled: React.PropTypes.bool,
/**
* The colors (at least one) that will be used to draw the refresh indicator.
* @platform android
*/
colors: React.PropTypes.arrayOf(React.PropTypes.string),
/**
* The background color of the refresh indicator.
* @platform android
*/
progressBackgroundColor: React.PropTypes.string,
/**
* Size of the refresh indicator, see PullToRefreshView.SIZE.
* @platform android
*/
size: React.PropTypes.oneOf(RefreshLayoutConsts.SIZE.DEFAULT, RefreshLayoutConsts.SIZE.LARGE),
},

render() {
if (Platform.OS === 'ios') {
return this._renderIOS();
} else if (Platform.OS === 'android') {
return this._renderAndroid();
}
},

_renderIOS() {
return (
<NativeRefreshControl
tintColor={this.props.tintColor}
title={this.props.title}
refreshing={this.props.refreshing}
onRefresh={this.props.onRefresh}/>
);
},

_renderAndroid() {
// On Android the ScrollView is wrapped so this component doesn't render
// anything and only acts as a way to configure the wrapper view.
// ScrollView will wrap itself in a AndroidSwipeRefreshLayout using props
// from this.
return null;
},
});

if (Platform.OS === 'ios') {
var NativeRefreshControl = requireNativeComponent(
'RCTRefreshControl',
RefreshControl
);
}

module.exports = RefreshControl;
45 changes: 44 additions & 1 deletion Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var insetsDiffer = require('insetsDiffer');
var invariant = require('invariant');
var pointsDiffer = require('pointsDiffer');
var requireNativeComponent = require('requireNativeComponent');
var processColor = require('processColor');

var PropTypes = React.PropTypes;

Expand Down Expand Up @@ -400,14 +401,29 @@ var ScrollView = React.createClass({
};
}

// Extract the RefreshControl from the children if there is one.
var refreshControl = null;
var children;
if (Array.isArray(this.props.children)) {
children = this.props.children.filter(c => {
if (c && c.type && c.type.displayName === 'RefreshControl') {
refreshControl = c;
return false;
}
return true;
});
} else {
children = this.props.children;
}

var contentContainer =
<View
{...contentSizeChangeProps}
ref={INNERVIEW}
style={contentContainerStyle}
removeClippedSubviews={this.props.removeClippedSubviews}
collapsable={false}>
{this.props.children}
{children}
</View>;

var alwaysBounceHorizontal =
Expand Down Expand Up @@ -466,6 +482,32 @@ var ScrollView = React.createClass({
'ScrollViewClass must not be undefined'
);

if (refreshControl) {
if (Platform.OS === 'ios') {
// On iOS the RefreshControl is a child of the ScrollView.
return (
<ScrollViewClass {...props} ref={SCROLLVIEW}>
{refreshControl}
{contentContainer}
</ScrollViewClass>
);
} else if (Platform.OS === 'android') {
// On Android wrap the ScrollView with a AndroidSwipeRefreshLayout.
// Since the ScrollView is wrapped add the style props to the
// AndroidSwipeRefreshLayout and use flex: 1 for the ScrollView.
var refreshProps = refreshControl.props;
return (
<AndroidSwipeRefreshLayout
{...refreshProps}
colors={refreshProps.colors && refreshProps.colors.map(processColor)}
style={props.style}>
<ScrollViewClass {...props} style={styles.base} ref={SCROLLVIEW}>
{contentContainer}
</ScrollViewClass>
</AndroidSwipeRefreshLayout>
);
}
}
return (
<ScrollViewClass {...props} ref={SCROLLVIEW}>
{contentContainer}
Expand Down Expand Up @@ -519,6 +561,7 @@ if (Platform.OS === 'android') {
'AndroidHorizontalScrollView',
ScrollView
);
var AndroidSwipeRefreshLayout = requireNativeComponent('AndroidSwipeRefreshLayout');
} else if (Platform.OS === 'ios') {
var RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView);
}
Expand Down
1 change: 1 addition & 0 deletions Libraries/react-native/react-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var ReactNative = {
get Switch() { return require('Switch'); },
get PullToRefreshViewAndroid() { return require('PullToRefreshViewAndroid'); },
get RecyclerViewBackedScrollView() { return require('RecyclerViewBackedScrollView'); },
get RefreshControl() { return require('RefreshControl'); },
get SwitchAndroid() { return require('SwitchAndroid'); },
get SwitchIOS() { return require('SwitchIOS'); },
get TabBarIOS() { return require('TabBarIOS'); },
Expand Down
3 changes: 2 additions & 1 deletion Libraries/react-native/react-native.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* and Flow doesn't have a good way to enable getters and setters for
* react-native without forcing all react-native users to also enable getters
* and setters. Until we solve that issue, we can use this .flow file to
* pretend like react-native doesn't use getters and setters
* pretend like react-native doesn't use getters and setters
*
* @flow
*/
Expand Down Expand Up @@ -47,6 +47,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
Switch: require('Switch'),
PullToRefreshViewAndroid: require('PullToRefreshViewAndroid'),
RecyclerViewBackedScrollView: require('RecyclerViewBackedScrollView'),
RefreshControl: require('RefreshControl'),
SwitchAndroid: require('SwitchAndroid'),
SwitchIOS: require('SwitchIOS'),
TabBarIOS: require('TabBarIOS'),
Expand Down
Loading

0 comments on commit c632f4d

Please sign in to comment.