Skip to content

Commit

Permalink
Perf improvements to RecyclerViewBackedScrollView.
Browse files Browse the repository at this point in the history
Differential Revision: D2641500

fb-gh-sync-id: 7ec6e2863bccebc98f75f586c0f17d509808d46b
  • Loading branch information
kmagiera authored and facebook-github-bot-8 committed Nov 11, 2015
1 parent 14b36b4 commit 5695ebd
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 42 deletions.
4 changes: 0 additions & 4 deletions Libraries/Components/ScrollResponder.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,10 +461,6 @@ var ScrollResponderMixin = {
this.addListenerOn(RCTDeviceEventEmitter, 'keyboardWillHide', this.scrollResponderKeyboardWillHide);
this.addListenerOn(RCTDeviceEventEmitter, 'keyboardDidShow', this.scrollResponderKeyboardDidShow);
this.addListenerOn(RCTDeviceEventEmitter, 'keyboardDidHide', this.scrollResponderKeyboardDidHide);
warning(this.getInnerViewNode, 'You need to implement getInnerViewNode in '
+ this.constructor.displayName + ' to get full'
+ 'functionality from ScrollResponder mixin. See example of ListView and'
+ ' ScrollView.');
},

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var NativeMethodsMixin = require('NativeMethodsMixin');
var React = require('React');
var ScrollResponder = require('ScrollResponder');
var ScrollView = require('ScrollView');
var View = require('View');
var StyleSheet = require('StyleSheet');

var requireNativeComponent = require('requireNativeComponent');

Expand Down Expand Up @@ -65,10 +67,6 @@ var RecyclerViewBackedScrollView = React.createClass({
return this;
},

getInnerViewNode: function(): any {
return React.findNodeHandle(this.refs[INNERVIEW]);
},

setNativeProps: function(props: Object) {
this.refs[INNERVIEW].setNativeProps(props);
},
Expand All @@ -93,11 +91,35 @@ var RecyclerViewBackedScrollView = React.createClass({
style: ([{flex: 1}, this.props.style]: ?Array<any>),
ref: INNERVIEW,
};

var wrappedChildren = React.Children.map(this.props.children, (child) => {
if (!child) {
return null;
}
return (
<View
collapsable={false}
style={styles.absolute}>
{child}
</View>
);
});

return (
<NativeAndroidRecyclerView {...props}/>
<NativeAndroidRecyclerView {...props}>
{wrappedChildren}
</NativeAndroidRecyclerView>
);
},
});

var styles = StyleSheet.create({
absolute: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
},
});

var NativeAndroidRecyclerView = requireNativeComponent('AndroidRecyclerViewBackedScrollView', null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
package com.facebook.react.views.recyclerview;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.content.Context;
import android.os.SystemClock;
Expand Down Expand Up @@ -84,10 +82,72 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
}
}

/**
* JavaScript ListView implementation rely on getting correct scroll offset. This class helps
* with calculating that "real" offset of items in recycler view as those are not provided by
* android widget implementation ({@link #onScrollChanged} is called with offset 0). We can't use
* onScrolled either as we need to take into account that if height of element that is not above
* the visible window changes the real scroll offset will change too, but onScrolled will only
* give us scroll deltas that comes from the user interaction.
*
* This class helps in calculating "real" offset of row at specified index. It's used from
* {@link #onScrollChanged} to query for the first visible index. Since while scrolling the
* queried index will usually increment or decrement by one it's optimize to return result in
* that common case very quickly.
*/
private static class ScrollOffsetTracker {

private final ReactListAdapter mReactListAdapter;

private int mLastRequestedPosition;
private int mOffsetForLastPosition;

private ScrollOffsetTracker(ReactListAdapter reactListAdapter) {
mReactListAdapter = reactListAdapter;
}

public void onHeightChange(int index, int oldHeight, int newHeight) {
if (index < mLastRequestedPosition) {
mOffsetForLastPosition = (mOffsetForLastPosition - oldHeight + newHeight);
}
}

public int getTopOffsetForItem(int index) {
if (mLastRequestedPosition != index) {
int sum = 0;
int startIndex = 0;
if (mLastRequestedPosition < index) {
if (mLastRequestedPosition != -1) {
sum = mOffsetForLastPosition;
startIndex = mLastRequestedPosition;
}
for (int i = startIndex; i < index; i++) {
sum += mReactListAdapter.mViews.get(i).getMeasuredHeight();
}
}
else {
if (index < (mLastRequestedPosition - index)) {
for (int i = 0; i < index; i++) {
sum += mReactListAdapter.mViews.get(i).getMeasuredHeight();
}
} else {
for (int i = mLastRequestedPosition - 1; i >= index; i--) {
sum -= mReactListAdapter.mViews.get(i).getMeasuredHeight();
}
}
}
mLastRequestedPosition = index;
mOffsetForLastPosition = sum;
}
return mOffsetForLastPosition;
}

}

/*package*/ static class ReactListAdapter extends Adapter<ConcreteViewHolder> {

private final List<View> mViews = new ArrayList<>();
private final Map<View, Integer> mTopOffsetsFromLayout = new HashMap<>();
private final ScrollOffsetTracker mScrollOffsetTracker;
private int mTotalChildrenHeight = 0;

// The following `OnLayoutChangeListsner` is attached to the views stored in the adapter
Expand All @@ -96,8 +156,6 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
private final View.OnLayoutChangeListener
mChildLayoutChangeListener = new View.OnLayoutChangeListener() {

private boolean mReentrant = false;

@Override
public void onLayoutChange(
View v,
Expand All @@ -109,27 +167,14 @@ public void onLayoutChange(
int oldTop,
int oldRight,
int oldBottom) {
// We need to get layout information from css-layout to set the size of the rows correctly
// and we also use top position that is calculated there to provide correct offset for the
// scroll events.
// To achieve both we first store updated top position. Then we call layout again to
// re-layout view at (0,0) position because each view cell needs a position in relative
// coordinates. To prevent from this event being triggered when we call layout again, we
// use `mReentrant` boolean as a guard.

if (!mReentrant) {
int oldHeight = (oldBottom - oldTop);
int newHeight = (bottom - top);
int width = right - left;

// Update top positions cache and total height
mTopOffsetsFromLayout.put(v, top);
mTotalChildrenHeight = mTotalChildrenHeight - oldHeight + newHeight;
// We need to get layout information from css-layout to set the size of the rows correctly.

int oldHeight = (oldBottom - oldTop);
int newHeight = (bottom - top);

// We need to re-layout view to place it in relative coordinates of cell wrapper -> (0,0)
mReentrant = true;
v.layout(0, 0, width, newHeight);
mReentrant = false;
if (oldHeight != newHeight) {
mTotalChildrenHeight = mTotalChildrenHeight - oldHeight + newHeight;
mScrollOffsetTracker.onHeightChange(mViews.indexOf(v), oldHeight, newHeight);

// Since "wrapper" view position +dimensions are not managed by NativeViewHierarchyManager
// we need to ensure that the wrapper view is properly layed out as it dimension should
Expand All @@ -142,7 +187,7 @@ public void onLayoutChange(
// update dimensions of them through overridden onMeasure method.
// We don't care about calling this is the view is not currently attached as it would be
// laid out once added to the recycler.
if (newHeight != oldHeight && v.getParent() != null
if (v.getParent() != null
&& v.getParent().getParent() != null) {
View wrapper = (View) v.getParent(); // native view that wraps view added to adapter
wrapper.forceLayout();
Expand All @@ -156,28 +201,27 @@ public void onLayoutChange(
};

public ReactListAdapter() {
mScrollOffsetTracker = new ScrollOffsetTracker(this);
setHasStableIds(true);
}

public void addView(View child, int index) {
mViews.add(index, child);

mTotalChildrenHeight += child.getMeasuredHeight();
mTopOffsetsFromLayout.put(child, child.getTop());
child.addOnLayoutChangeListener(mChildLayoutChangeListener);

notifyDataSetChanged();
notifyItemInserted(index);
}

public void removeViewAt(int index) {
View child = mViews.get(index);
if (child != null) {
mViews.remove(index);
mTopOffsetsFromLayout.remove(child);
child.removeOnLayoutChangeListener(mChildLayoutChangeListener);
mTotalChildrenHeight -= child.getMeasuredHeight();

notifyDataSetChanged();
notifyItemRemoved(index);
}
}

Expand Down Expand Up @@ -220,16 +264,15 @@ public int getTotalChildrenHeight() {
}

public int getTopOffsetForItem(int index) {
return Assertions.assertNotNull(
mTopOffsetsFromLayout.get(Assertions.assertNotNull(mViews.get(index))));
return mScrollOffsetTracker.getTopOffsetForItem(index);
}
}

private int calculateAbsoluteOffset() {
int offsetY = 0;
if (getChildCount() > 0) {
View recyclerViewChild = getChildAt(0);
int childPosition = getChildAdapterPosition(recyclerViewChild);
int childPosition = getChildViewHolder(recyclerViewChild).getLayoutPosition();
offsetY = ((ReactListAdapter) getAdapter()).getTopOffsetForItem(childPosition) -
recyclerViewChild.getTop();
}
Expand Down

0 comments on commit 5695ebd

Please sign in to comment.