diff --git a/Libraries/Components/ScrollView/RecyclerViewBackedScrollView.android.js b/Libraries/Components/ScrollView/RecyclerViewBackedScrollView.android.js index c7b244a63..fa019c6be 100644 --- a/Libraries/Components/ScrollView/RecyclerViewBackedScrollView.android.js +++ b/Libraries/Components/ScrollView/RecyclerViewBackedScrollView.android.js @@ -71,6 +71,11 @@ var RecyclerViewBackedScrollView = React.createClass({ this.refs[INNERVIEW].setNativeProps(props); }, + _handleContentSizeChange: function(event) { + var {width, height} = event.nativeEvent; + this.props.onContentSizeChange(width, height); + }, + render: function() { var props = { ...this.props, @@ -92,6 +97,10 @@ var RecyclerViewBackedScrollView = React.createClass({ ref: INNERVIEW, }; + if (this.props.onContentSizeChange) { + props.onContentSizeChange = this._handleContentSizeChange; + } + var wrappedChildren = React.Children.map(this.props.children, (child) => { if (!child) { return null; diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 7a2dc58e7..4d90f19d6 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -193,6 +193,12 @@ var ScrollView = React.createClass({ * @platform ios */ onScrollAnimationEnd: PropTypes.func, + /** + * Called when scrollable content view of the ScrollView changes. It's + * implemented using onLayout handler attached to the content container + * which this ScrollView renders. + */ + onContentSizeChange: PropTypes.func, /** * When true, the scroll view stops on multiples of the scroll view's size * when scrolling. This can be used for horizontal pagination. The default @@ -360,6 +366,11 @@ var ScrollView = React.createClass({ this.scrollResponderHandleScroll(e); }, + _handleContentOnLayout: function(event) { + var {width, height} = event.nativeEvent.layout; + this.props.onContentSizeChange && this.props.onContentSizeChange(width, height); + }, + render: function() { var contentContainerStyle = [ this.props.horizontal && styles.contentContainerHorizontal, @@ -376,8 +387,16 @@ var ScrollView = React.createClass({ ); } + var contentSizeChangeProps = {}; + if (this.props.onContentSizeChange) { + contentSizeChangeProps = { + onLayout: this._handleContentOnLayout, + }; + } + var contentContainer = { + + public static final String EVENT_NAME = "topContentSizeChange"; + + private final int mWidth; + private final int mHeight; + + public ContentSizeChangeEvent(int viewTag, long timestampMs, int width, int height) { + super(viewTag, timestampMs); + mWidth = width; + mHeight = height; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + WritableMap data = Arguments.createMap(); + data.putDouble("width", PixelUtil.toDIPFromPixel(mWidth)); + data.putDouble("height", PixelUtil.toDIPFromPixel(mHeight)); + rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java index a419b58c3..054a95222 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java @@ -148,6 +148,7 @@ public class RecyclerViewBackedScrollView extends RecyclerView { private final List mViews = new ArrayList<>(); private final ScrollOffsetTracker mScrollOffsetTracker; + private final RecyclerViewBackedScrollView mScrollView; private int mTotalChildrenHeight = 0; // The following `OnLayoutChangeListsner` is attached to the views stored in the adapter @@ -173,7 +174,7 @@ public class RecyclerViewBackedScrollView extends RecyclerView { int newHeight = (bottom - top); if (oldHeight != newHeight) { - mTotalChildrenHeight = mTotalChildrenHeight - oldHeight + newHeight; + updateTotalChildrenHeight(newHeight - oldHeight); mScrollOffsetTracker.onHeightChange(mViews.indexOf(v), oldHeight, newHeight); // Since "wrapper" view position +dimensions are not managed by NativeViewHierarchyManager @@ -200,7 +201,8 @@ public class RecyclerViewBackedScrollView extends RecyclerView { } }; - public ReactListAdapter() { + public ReactListAdapter(RecyclerViewBackedScrollView scrollView) { + mScrollView = scrollView; mScrollOffsetTracker = new ScrollOffsetTracker(this); setHasStableIds(true); } @@ -208,7 +210,7 @@ public class RecyclerViewBackedScrollView extends RecyclerView { public void addView(View child, int index) { mViews.add(index, child); - mTotalChildrenHeight += child.getMeasuredHeight(); + updateTotalChildrenHeight(child.getMeasuredHeight()); child.addOnLayoutChangeListener(mChildLayoutChangeListener); notifyItemInserted(index); @@ -219,12 +221,19 @@ public class RecyclerViewBackedScrollView extends RecyclerView { if (child != null) { mViews.remove(index); child.removeOnLayoutChangeListener(mChildLayoutChangeListener); - mTotalChildrenHeight -= child.getMeasuredHeight(); + updateTotalChildrenHeight(-child.getMeasuredHeight()); notifyItemRemoved(index); } } + private void updateTotalChildrenHeight(int delta) { + if (delta != 0) { + mTotalChildrenHeight += delta; + mScrollView.onTotalChildrenHeightChange(mTotalChildrenHeight); + } + } + @Override public ConcreteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ConcreteViewHolder(new RecyclableWrapperViewGroup(parent.getContext())); @@ -268,6 +277,12 @@ public class RecyclerViewBackedScrollView extends RecyclerView { } } + private boolean mSendContentSizeChangeEvents; + + public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) { + mSendContentSizeChangeEvents = sendContentSizeChangeEvents; + } + private int calculateAbsoluteOffset() { int offsetY = 0; if (getChildCount() > 0) { @@ -304,12 +319,23 @@ public class RecyclerViewBackedScrollView extends RecyclerView { getHeight())); } + private void onTotalChildrenHeightChange(int newTotalChildrenHeight) { + if (mSendContentSizeChangeEvents) { + ((ReactContext) getContext()).getNativeModule(UIManagerModule.class).getEventDispatcher() + .dispatchEvent(new ContentSizeChangeEvent( + getId(), + SystemClock.uptimeMillis(), + getWidth(), + newTotalChildrenHeight)); + } + } + public RecyclerViewBackedScrollView(Context context) { super(context); setHasFixedSize(true); setItemAnimator(new NotAnimatedItemAnimator()); setLayoutManager(new LinearLayoutManager(context)); - setAdapter(new ReactListAdapter()); + setAdapter(new ReactListAdapter(this)); } /*package*/ void addViewToAdapter(View child, int index) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java index 8c0134dad..c49c26742 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java @@ -4,12 +4,17 @@ package com.facebook.react.views.recyclerview; import javax.annotation.Nullable; +import java.util.Map; + import android.view.View; 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.ViewGroupManager; import com.facebook.react.views.scroll.ReactScrollViewCommandHelper; +import com.facebook.react.views.scroll.ScrollEvent; /** * View manager for {@link RecyclerViewBackedScrollView}. @@ -27,6 +32,11 @@ public class RecyclerViewBackedScrollViewManager extends // TODO(8624925): Implement removeClippedSubviews support for native ListView + @ReactProp(name = "onContentSizeChange") + public void setOnContentSizeChange(RecyclerViewBackedScrollView view, boolean value) { + view.setSendContentSizeChangeEvents(value); + } + @Override protected RecyclerViewBackedScrollView createViewInstance(ThemedReactContext reactContext) { return new RecyclerViewBackedScrollView(reactContext); @@ -76,4 +86,15 @@ public class RecyclerViewBackedScrollViewManager extends ReactScrollViewCommandHelper.ScrollToCommandData data) { view.scrollTo(data.mDestX, data.mDestY, false); } + + @Override + public @Nullable + Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.builder() + .put(ScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onScroll")) + .put( + ContentSizeChangeEvent.EVENT_NAME, + MapBuilder.of("registrationName", "onContentSizeChange")) + .build(); + } }