From 1195f9c8e8e47c25d90993218f9ea63123d71cc3 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Fri, 20 Nov 2015 07:36:23 -0800 Subject: [PATCH] Further improvements in RecyclerViewBackedScrollView. Summary: public Changed ListView to use onLayout and onContentSizeChange (new) events instead of measure. Updated ScrollView implementation to support contentSizeChange event with an implementation based on onLayout attached to the content view. For RecyclerViewBackedScrollView we need to generate that event directly as it doesn't have a concept of content view. This greatly improves performance of ListView that uses RecyclerViewBackedScrollView Reviewed By: mkonicek Differential Revision: D2679460 fb-gh-sync-id: ba26462d9d3b071965cbe46314f89f0dcfd9db9f --- .../RecyclerViewBackedScrollView.android.js | 9 +++++ Libraries/Components/ScrollView/ScrollView.js | 19 +++++++++ .../CustomComponents/ListView/ListView.js | 25 ++++++------ .../recyclerview/ContentSizeChangeEvent.java | 40 +++++++++++++++++++ .../RecyclerViewBackedScrollView.java | 36 ++++++++++++++--- .../RecyclerViewBackedScrollViewManager.java | 21 ++++++++++ 6 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/ContentSizeChangeEvent.java 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(); + } }