diff --git a/Examples/UIExplorer/PullToRefreshLayoutAndroidExample.android.js b/Examples/UIExplorer/PullToRefreshLayoutAndroidExample.android.js
new file mode 100644
index 000000000..06a39c7cc
--- /dev/null
+++ b/Examples/UIExplorer/PullToRefreshLayoutAndroidExample.android.js
@@ -0,0 +1,127 @@
+/**
+* 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,
+ PullToRefreshLayoutAndroid,
+ Text,
+ TouchableWithoutFeedback,
+ View,
+} = React;
+
+const styles = StyleSheet.create({
+ row: {
+ borderColor: 'grey',
+ borderWidth: 1,
+ padding: 20,
+ backgroundColor: '#3a5795',
+ margin: 5,
+ },
+ text: {
+ alignSelf: 'center',
+ color: '#fff',
+
+ },
+ layout: {
+ flex: 1,
+ },
+ scrollview: {
+ flex: 1,
+ },
+});
+
+const Row = React.createClass({
+ _onClick: function() {
+ this.props.onClick(this.props.data);
+ },
+ render: function() {
+ return (
+
+
+
+ {this.props.data.text + ' (' + this.props.data.clicks + ' clicks)'}
+
+
+
+ );
+ },
+});
+const PullToRefreshLayoutAndroidExample = React.createClass({
+ statics: {
+ title: '',
+ description: 'Container that adds pull-to-refresh support to its child view.'
+ },
+
+ getInitialState() {
+ return {
+ isRefreshing: false,
+ loaded: 0,
+ rowData: Array.from(new Array(20)).map(
+ (val, i) => {return {text: 'Initial row' + i, clicks: 0}}),
+ };
+ },
+
+ _onClick(row) {
+ row.clicks++;
+ this.setState({
+ rowData: this.state.rowData,
+ });
+ },
+
+ render() {
+ const rows = this.state.rowData.map((row) => {
+ return ;
+ });
+ return (
+
+
+ {rows}
+
+
+ );
+ },
+
+ _onRefresh() {
+ this.setState({isRefreshing: true});
+ setTimeout(() => {
+ // prepend 10 items
+ const rowData = Array.from(new Array(10))
+ .map((val, i) => {return {
+ 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 = PullToRefreshLayoutAndroidExample;
diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js
index 5a370a01c..7d57332c2 100644
--- a/Examples/UIExplorer/UIExplorerList.android.js
+++ b/Examples/UIExplorer/UIExplorerList.android.js
@@ -27,6 +27,7 @@ var COMPONENTS = [
require('./ProgressBarAndroidExample'),
require('./ScrollViewSimpleExample'),
require('./SwitchAndroidExample'),
+ require('./PullToRefreshLayoutAndroidExample.android'),
require('./TextExample.android'),
require('./TextInputExample.android'),
require('./ToolbarAndroidExample'),
diff --git a/Libraries/PullToRefresh/PullToRefreshLayoutAndroid.android.js b/Libraries/PullToRefresh/PullToRefreshLayoutAndroid.android.js
new file mode 100644
index 000000000..a1d69c353
--- /dev/null
+++ b/Libraries/PullToRefresh/PullToRefreshLayoutAndroid.android.js
@@ -0,0 +1,87 @@
+/**
+ * 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 PullToRefreshLayoutAndroid
+ */
+'use strict';
+
+var React = require('React');
+var RefreshLayoutConsts = require('NativeModules').UIManager.AndroidSwipeRefreshLayout.Constants;
+var View = require('View');
+
+var onlyChild = require('onlyChild');
+var processColor = require('processColor');
+var requireNativeComponent = require('requireNativeComponent');
+
+var NATIVE_REF = 'native_swiperefreshlayout';
+
+/**
+ * React view that supports a single scrollable child view (e.g. `ScrollView`). When this child
+ * view is at `scrollY: 0`, swiping down triggers an `onRefresh` event.
+ */
+var PullToRefreshLayoutAndroid = React.createClass({
+ statics: {
+ SIZE: RefreshLayoutConsts.SIZE,
+ },
+
+ propTypes: {
+ ...View.propTypes,
+ /**
+ * Whether the pull to refresh functionality is enabled
+ */
+ enabled: React.PropTypes.bool,
+ /**
+ * The colors (at least one) that will be used to draw the refresh indicator
+ */
+ colors: React.PropTypes.arrayOf(React.PropTypes.string),
+ /**
+ * The background color of the refresh indicator
+ */
+ progressBackgroundColor: React.PropTypes.string,
+ /**
+ * Whether the view should be indicating an active refresh
+ */
+ refreshing: React.PropTypes.bool,
+ /**
+ * Size of the refresh indicator, see PullToRefreshLayoutAndroid.SIZE
+ */
+ size: React.PropTypes.oneOf(RefreshLayoutConsts.SIZE.DEFAULT, RefreshLayoutConsts.SIZE.LARGE),
+ },
+
+ getInnerViewNode: function() {
+ return this.refs[NATIVE_REF];
+ },
+
+ render: function() {
+ return (
+
+ {onlyChild(this.props.children)}
+
+ );
+ },
+
+ _onRefresh: function() {
+ this.props.onRefresh && this.props.onRefresh();
+ this.refs[NATIVE_REF].setNativeProps({refreshing: !!this.props.refreshing});
+ }
+});
+
+var NativePullToRefresh = requireNativeComponent(
+ 'AndroidSwipeRefreshLayout',
+ PullToRefreshLayoutAndroid
+);
+
+module.exports = PullToRefreshLayoutAndroid;
diff --git a/Libraries/PullToRefresh/PullToRefreshLayoutAndroid.ios.js b/Libraries/PullToRefresh/PullToRefreshLayoutAndroid.ios.js
new file mode 100644
index 000000000..eea4dbae7
--- /dev/null
+++ b/Libraries/PullToRefresh/PullToRefreshLayoutAndroid.ios.js
@@ -0,0 +1,13 @@
+/**
+ * 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 PullToRefreshLayoutAndroid
+ */
+ 'use strict';
+
+ module.exports = require('UnimplementedView');
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 206b5f785..29ff95205 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -37,6 +37,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
SliderIOS: require('SliderIOS'),
SnapshotViewIOS: require('SnapshotViewIOS'),
Switch: require('Switch'),
+ PullToRefreshLayoutAndroid: require('PullToRefreshLayoutAndroid'),
SwitchAndroid: require('SwitchAndroid'),
SwitchIOS: require('SwitchIOS'),
TabBarIOS: require('TabBarIOS'),
diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
index 115c33ea6..a0e90db20 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java
@@ -39,6 +39,7 @@ import com.facebook.react.views.textinput.ReactTextInputManager;
import com.facebook.react.views.toolbar.ReactToolbarManager;
import com.facebook.react.views.view.ReactViewManager;
import com.facebook.react.views.viewpager.ReactViewPagerManager;
+import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager;
/**
* Package defining basic modules and view managers.
@@ -78,6 +79,7 @@ public class MainReactPackage implements ReactPackage {
new ReactViewManager(),
new ReactViewPagerManager(),
new ReactTextInlineImageViewManager(),
- new ReactVirtualTextViewManager());
+ new ReactVirtualTextViewManager(),
+ new SwipeRefreshLayoutManager());
}
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java
new file mode 100644
index 000000000..f458c0ab0
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.java
@@ -0,0 +1,35 @@
+/**
+ * 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.
+ */
+
+package com.facebook.react.views.swiperefresh;
+
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.view.MotionEvent;
+
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.uimanager.events.NativeGestureUtil;
+
+/**
+ * Basic extension of {@link SwipeRefreshLayout} with ReactNative-specific functionality.
+ */
+public class ReactSwipeRefreshLayout extends SwipeRefreshLayout {
+
+ public ReactSwipeRefreshLayout(ReactContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (super.onInterceptTouchEvent(ev)) {
+ NativeGestureUtil.notifyNativeGestureStarted(this, ev);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/RefreshEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/RefreshEvent.java
new file mode 100644
index 000000000..d8325d2bc
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/RefreshEvent.java
@@ -0,0 +1,30 @@
+/**
+ * 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.
+ */
+
+package com.facebook.react.views.swiperefresh;
+
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+public class RefreshEvent extends Event {
+
+ protected RefreshEvent(int viewTag, long timestampMs) {
+ super(viewTag, timestampMs);
+ }
+
+ @Override
+ public String getEventName() {
+ return "topRefresh";
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), null);
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java
new file mode 100644
index 000000000..1520dc1f9
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java
@@ -0,0 +1,106 @@
+/**
+ * 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.
+ */
+
+package com.facebook.react.views.swiperefresh;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+
+import android.graphics.Color;
+import android.os.SystemClock;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.ReactProp;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.ViewProps;
+
+/**
+ * ViewManager for {@link ReactSwipeRefreshLayout} which allows the user to "pull to refresh" a
+ * child view. Emits an {@code onRefresh} event when this happens.
+ */
+public class SwipeRefreshLayoutManager extends ViewGroupManager {
+
+ @Override
+ protected ReactSwipeRefreshLayout createViewInstance(ThemedReactContext reactContext) {
+ return new ReactSwipeRefreshLayout(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return "AndroidSwipeRefreshLayout";
+ }
+
+ @ReactProp(name = ViewProps.ENABLED, defaultBoolean = true)
+ public void setEnabled(ReactSwipeRefreshLayout view, boolean enabled) {
+ view.setEnabled(enabled);
+ }
+
+ @ReactProp(name = "colors")
+ public void setColors(ReactSwipeRefreshLayout view, @Nullable ReadableArray colors) {
+ if (colors != null) {
+ int[] colorValues = new int[colors.size()];
+ for (int i = 0; i < colors.size(); i++) {
+ colorValues[i] = colors.getInt(i);
+ }
+ view.setColorSchemeColors(colorValues);
+ } else {
+ view.setColorSchemeColors();
+ }
+ }
+
+ @ReactProp(name = "progressBackgroundColor", defaultInt = Color.TRANSPARENT, customType = "Color")
+ public void setProgressBackgroundColor(ReactSwipeRefreshLayout view, int color) {
+ view.setProgressBackgroundColorSchemeColor(color);
+ }
+
+ @ReactProp(name = "size", defaultInt = SwipeRefreshLayout.DEFAULT)
+ public void setSize(ReactSwipeRefreshLayout view, int size) {
+ view.setSize(size);
+ }
+
+ @ReactProp(name = "refreshing")
+ public void setRefreshing(ReactSwipeRefreshLayout view, boolean refreshing) {
+ view.setRefreshing(refreshing);
+ }
+
+ @Override
+ protected void addEventEmitters(
+ final ThemedReactContext reactContext,
+ final ReactSwipeRefreshLayout view) {
+ view.setOnRefreshListener(
+ new OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
+ .dispatchEvent(new RefreshEvent(view.getId(), SystemClock.uptimeMillis()));
+ }
+ });
+ }
+
+ @Nullable
+ @Override
+ public Map getExportedViewConstants() {
+ return MapBuilder.of(
+ "SIZE",
+ MapBuilder.of("DEFAULT", SwipeRefreshLayout.DEFAULT, "LARGE", SwipeRefreshLayout.LARGE));
+ }
+
+ @Override
+ public Map getExportedCustomDirectEventTypeConstants() {
+ return MapBuilder.builder()
+ .put("topRefresh", MapBuilder.of("registrationName", "onRefresh"))
+ .build();
+ }
+}