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);
},
_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;

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

@ -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 =
<View
{...contentSizeChangeProps}
ref={INNERVIEW}
style={contentContainerStyle}
removeClippedSubviews={this.props.removeClippedSubviews}

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

@ -406,6 +406,8 @@ var ListView = React.createClass({
// component's original ref instead of clobbering it
return React.cloneElement(renderScrollComponent(props), {
ref: SCROLLVIEW_REF,
onContentSizeChange: this._onContentSizeChange,
onLayout: this._onLayout,
}, header, bodyComponents, footer);
},
@ -418,17 +420,6 @@ var ListView = React.createClass({
if (!scrollComponent || !scrollComponent.getInnerViewNode) {
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
// 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 ?
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) {

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

@ -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 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) {

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

@ -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();
}
}