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
This commit is contained in:
Krzysztof Magiera 2015-11-20 07:36:23 -08:00 коммит произвёл facebook-github-bot-6
Родитель 848a151ff8
Коммит 1195f9c8e8
6 изменённых файлов: 133 добавлений и 17 удалений

Просмотреть файл

@ -71,6 +71,11 @@ var RecyclerViewBackedScrollView = React.createClass({
this.refs[INNERVIEW].setNativeProps(props); this.refs[INNERVIEW].setNativeProps(props);
}, },
_handleContentSizeChange: function(event) {
var {width, height} = event.nativeEvent;
this.props.onContentSizeChange(width, height);
},
render: function() { render: function() {
var props = { var props = {
...this.props, ...this.props,
@ -92,6 +97,10 @@ var RecyclerViewBackedScrollView = React.createClass({
ref: INNERVIEW, ref: INNERVIEW,
}; };
if (this.props.onContentSizeChange) {
props.onContentSizeChange = this._handleContentSizeChange;
}
var wrappedChildren = React.Children.map(this.props.children, (child) => { var wrappedChildren = React.Children.map(this.props.children, (child) => {
if (!child) { if (!child) {
return null; return null;

Просмотреть файл

@ -193,6 +193,12 @@ var ScrollView = React.createClass({
* @platform ios * @platform ios
*/ */
onScrollAnimationEnd: PropTypes.func, 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 true, the scroll view stops on multiples of the scroll view's size
* when scrolling. This can be used for horizontal pagination. The default * when scrolling. This can be used for horizontal pagination. The default
@ -360,6 +366,11 @@ var ScrollView = React.createClass({
this.scrollResponderHandleScroll(e); this.scrollResponderHandleScroll(e);
}, },
_handleContentOnLayout: function(event) {
var {width, height} = event.nativeEvent.layout;
this.props.onContentSizeChange && this.props.onContentSizeChange(width, height);
},
render: function() { render: function() {
var contentContainerStyle = [ var contentContainerStyle = [
this.props.horizontal && styles.contentContainerHorizontal, 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 = var contentContainer =
<View <View
{...contentSizeChangeProps}
ref={INNERVIEW} ref={INNERVIEW}
style={contentContainerStyle} style={contentContainerStyle}
removeClippedSubviews={this.props.removeClippedSubviews} removeClippedSubviews={this.props.removeClippedSubviews}

Просмотреть файл

@ -406,6 +406,8 @@ var ListView = React.createClass({
// component's original ref instead of clobbering it // component's original ref instead of clobbering it
return React.cloneElement(renderScrollComponent(props), { return React.cloneElement(renderScrollComponent(props), {
ref: SCROLLVIEW_REF, ref: SCROLLVIEW_REF,
onContentSizeChange: this._onContentSizeChange,
onLayout: this._onLayout,
}, header, bodyComponents, footer); }, header, bodyComponents, footer);
}, },
@ -418,17 +420,6 @@ var ListView = React.createClass({
if (!scrollComponent || !scrollComponent.getInnerViewNode) { if (!scrollComponent || !scrollComponent.getInnerViewNode) {
return; return;
} }
RCTUIManager.measureLayout(
scrollComponent.getInnerViewNode(),
React.findNodeHandle(scrollComponent),
logError,
this._setScrollContentLength
);
RCTUIManager.measureLayoutRelativeToParent(
React.findNodeHandle(scrollComponent),
logError,
this._setScrollVisibleLength
);
// RCTScrollViewManager.calculateChildFrames is not available on // RCTScrollViewManager.calculateChildFrames is not available on
// every platform // every platform
@ -439,9 +430,19 @@ var ListView = React.createClass({
); );
}, },
_setScrollContentLength: function(left, top, width, height) { _onContentSizeChange: function(width, height) {
this.scrollProperties.contentLength = !this.props.horizontal ? this.scrollProperties.contentLength = !this.props.horizontal ?
height : width; height : width;
this._updateVisibleRows();
this._renderMoreRowsIfNeeded();
},
_onLayout: function(event) {
var {width, height} = event.nativeEvent.layout;
this.scrollProperties.visibleLength = !this.props.horizontal ?
height : width;
this._updateVisibleRows();
this._renderMoreRowsIfNeeded();
}, },
_setScrollVisibleLength: function(left, top, width, height) { _setScrollVisibleLength: function(left, top, width, height) {

Просмотреть файл

@ -0,0 +1,40 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.views.recyclerview;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
/**
* Event dispatched by {@link RecyclerViewBackedScrollView} when total height of it's children
* changes
*/
public class ContentSizeChangeEvent extends Event<ContentSizeChangeEvent> {
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);
}
}

Просмотреть файл

@ -148,6 +148,7 @@ public class RecyclerViewBackedScrollView extends RecyclerView {
private final List<View> mViews = new ArrayList<>(); private final List<View> mViews = new ArrayList<>();
private final ScrollOffsetTracker mScrollOffsetTracker; private final ScrollOffsetTracker mScrollOffsetTracker;
private final RecyclerViewBackedScrollView mScrollView;
private int mTotalChildrenHeight = 0; private int mTotalChildrenHeight = 0;
// The following `OnLayoutChangeListsner` is attached to the views stored in the adapter // 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); int newHeight = (bottom - top);
if (oldHeight != newHeight) { if (oldHeight != newHeight) {
mTotalChildrenHeight = mTotalChildrenHeight - oldHeight + newHeight; updateTotalChildrenHeight(newHeight - oldHeight);
mScrollOffsetTracker.onHeightChange(mViews.indexOf(v), oldHeight, newHeight); mScrollOffsetTracker.onHeightChange(mViews.indexOf(v), oldHeight, newHeight);
// Since "wrapper" view position +dimensions are not managed by NativeViewHierarchyManager // 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); mScrollOffsetTracker = new ScrollOffsetTracker(this);
setHasStableIds(true); setHasStableIds(true);
} }
@ -208,7 +210,7 @@ public class RecyclerViewBackedScrollView extends RecyclerView {
public void addView(View child, int index) { public void addView(View child, int index) {
mViews.add(index, child); mViews.add(index, child);
mTotalChildrenHeight += child.getMeasuredHeight(); updateTotalChildrenHeight(child.getMeasuredHeight());
child.addOnLayoutChangeListener(mChildLayoutChangeListener); child.addOnLayoutChangeListener(mChildLayoutChangeListener);
notifyItemInserted(index); notifyItemInserted(index);
@ -219,12 +221,19 @@ public class RecyclerViewBackedScrollView extends RecyclerView {
if (child != null) { if (child != null) {
mViews.remove(index); mViews.remove(index);
child.removeOnLayoutChangeListener(mChildLayoutChangeListener); child.removeOnLayoutChangeListener(mChildLayoutChangeListener);
mTotalChildrenHeight -= child.getMeasuredHeight(); updateTotalChildrenHeight(-child.getMeasuredHeight());
notifyItemRemoved(index); notifyItemRemoved(index);
} }
} }
private void updateTotalChildrenHeight(int delta) {
if (delta != 0) {
mTotalChildrenHeight += delta;
mScrollView.onTotalChildrenHeightChange(mTotalChildrenHeight);
}
}
@Override @Override
public ConcreteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public ConcreteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ConcreteViewHolder(new RecyclableWrapperViewGroup(parent.getContext())); 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() { private int calculateAbsoluteOffset() {
int offsetY = 0; int offsetY = 0;
if (getChildCount() > 0) { if (getChildCount() > 0) {
@ -304,12 +319,23 @@ public class RecyclerViewBackedScrollView extends RecyclerView {
getHeight())); 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) { public RecyclerViewBackedScrollView(Context context) {
super(context); super(context);
setHasFixedSize(true); setHasFixedSize(true);
setItemAnimator(new NotAnimatedItemAnimator()); setItemAnimator(new NotAnimatedItemAnimator());
setLayoutManager(new LinearLayoutManager(context)); setLayoutManager(new LinearLayoutManager(context));
setAdapter(new ReactListAdapter()); setAdapter(new ReactListAdapter(this));
} }
/*package*/ void addViewToAdapter(View child, int index) { /*package*/ void addViewToAdapter(View child, int index) {

Просмотреть файл

@ -4,12 +4,17 @@ package com.facebook.react.views.recyclerview;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Map;
import android.view.View; import android.view.View;
import com.facebook.react.bridge.ReadableArray; 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.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.views.scroll.ReactScrollViewCommandHelper; import com.facebook.react.views.scroll.ReactScrollViewCommandHelper;
import com.facebook.react.views.scroll.ScrollEvent;
/** /**
* View manager for {@link RecyclerViewBackedScrollView}. * View manager for {@link RecyclerViewBackedScrollView}.
@ -27,6 +32,11 @@ public class RecyclerViewBackedScrollViewManager extends
// TODO(8624925): Implement removeClippedSubviews support for native ListView // TODO(8624925): Implement removeClippedSubviews support for native ListView
@ReactProp(name = "onContentSizeChange")
public void setOnContentSizeChange(RecyclerViewBackedScrollView view, boolean value) {
view.setSendContentSizeChangeEvents(value);
}
@Override @Override
protected RecyclerViewBackedScrollView createViewInstance(ThemedReactContext reactContext) { protected RecyclerViewBackedScrollView createViewInstance(ThemedReactContext reactContext) {
return new RecyclerViewBackedScrollView(reactContext); return new RecyclerViewBackedScrollView(reactContext);
@ -76,4 +86,15 @@ public class RecyclerViewBackedScrollViewManager extends
ReactScrollViewCommandHelper.ScrollToCommandData data) { ReactScrollViewCommandHelper.ScrollToCommandData data) {
view.scrollTo(data.mDestX, data.mDestY, false); 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();
}
} }