BREAKING: Move RecyclerViewBackedScrollView out of open source
Summary: `RecyclerViewBackedScrollView` was added a long time ago to work around the scroll-back-when-data-is-added bug, but that has now been fixed directly in the `ScrollView` (`ReactScrollView.java`) in open source and internally. Reviewed By: astreet Differential Revision: D4482105 fbshipit-source-id: 208f21f00045d5c5a83b74ad69b3db6fa63391d7
This commit is contained in:
parent
3ee3d2b4b2
commit
6ec5654e7a
|
@ -1,189 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
|
||||||
*
|
|
||||||
* @providesModule RecyclerViewBackedScrollView
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var React = require('React');
|
|
||||||
var ScrollResponder = require('ScrollResponder');
|
|
||||||
var ScrollView = require('ScrollView');
|
|
||||||
var View = require('View');
|
|
||||||
var StyleSheet = require('StyleSheet');
|
|
||||||
|
|
||||||
var requireNativeComponent = require('requireNativeComponent');
|
|
||||||
|
|
||||||
var INNERVIEW = 'InnerView';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RecyclerViewBackedScrollView is DEPRECATED and will be removed from
|
|
||||||
* React Native.
|
|
||||||
* Please use a `ListView` which has `removeClippedSubviews` enabled by
|
|
||||||
* default so that rows that are out of sight are automatically
|
|
||||||
* detached from the view hierarchy.
|
|
||||||
*
|
|
||||||
* Wrapper around Android native recycler view.
|
|
||||||
*
|
|
||||||
* It simply renders rows passed as children in a separate recycler view cells
|
|
||||||
* similarly to how `ScrollView` is doing it. Thanks to the fact that it uses
|
|
||||||
* native `RecyclerView` though, rows that are out of sight are going to be
|
|
||||||
* automatically detached (similarly on how this would work with
|
|
||||||
* `removeClippedSubviews = true` on a `ScrollView.js`).
|
|
||||||
*
|
|
||||||
* CAUTION: This is an experimental component and should only be used together
|
|
||||||
* with javascript implementation of list view (see ListView.js). In order to
|
|
||||||
* use it pass this component as `renderScrollComponent` to the list view. For
|
|
||||||
* now only horizontal scrolling is supported.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* getInitialState: function() {
|
|
||||||
* var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
|
|
||||||
* return {
|
|
||||||
* dataSource: ds.cloneWithRows(['row 1', 'row 2']),
|
|
||||||
* };
|
|
||||||
* },
|
|
||||||
*
|
|
||||||
* render: function() {
|
|
||||||
* return (
|
|
||||||
* <ListView
|
|
||||||
* dataSource={this.state.dataSource}
|
|
||||||
* renderRow={rowData => <Text>{rowData}</Text>}
|
|
||||||
* renderScrollComponent={props => <RecyclerViewBackedScrollView {...props} />}
|
|
||||||
* />
|
|
||||||
* );
|
|
||||||
* },
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
var RecyclerViewBackedScrollView = React.createClass({
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
...ScrollView.propTypes,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [ScrollResponder.Mixin],
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
|
||||||
console.warn(
|
|
||||||
'RecyclerViewBackedScrollView is DEPRECATED and will be removed from React Native. ' +
|
|
||||||
'Please use a ListView which has removeClippedSubviews enabled by default so that ' +
|
|
||||||
'rows that are out of sight are automatically detached from the view hierarchy.')
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return this.scrollResponderMixinGetInitialState();
|
|
||||||
},
|
|
||||||
|
|
||||||
getScrollResponder: function() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
setNativeProps: function(props: Object) {
|
|
||||||
this.refs[INNERVIEW].setNativeProps(props);
|
|
||||||
},
|
|
||||||
|
|
||||||
_handleContentSizeChange: function(event) {
|
|
||||||
var {width, height} = event.nativeEvent;
|
|
||||||
this.props.onContentSizeChange(width, height);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper function to scroll to a specific point in the scrollview.
|
|
||||||
* This is currently used to help focus on child textviews, but can also
|
|
||||||
* be used to quickly scroll to any element we want to focus. Syntax:
|
|
||||||
*
|
|
||||||
* scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})
|
|
||||||
*
|
|
||||||
* Note: The weird argument signature is due to the fact that, for historical reasons,
|
|
||||||
* the function also accepts separate arguments as as alternative to the options object.
|
|
||||||
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
|
|
||||||
*/
|
|
||||||
scrollTo: function(
|
|
||||||
y?: number | { x?: number, y?: number, animated?: boolean },
|
|
||||||
x?: number,
|
|
||||||
animated?: boolean
|
|
||||||
) {
|
|
||||||
if (typeof y === 'number') {
|
|
||||||
console.warn('`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, animated: true})` instead.');
|
|
||||||
} else {
|
|
||||||
({x, y, animated} = y || {});
|
|
||||||
}
|
|
||||||
this.getScrollResponder().scrollResponderScrollTo({x: x || 0, y: y || 0, animated: animated !== false});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var recyclerProps = {
|
|
||||||
...this.props,
|
|
||||||
onTouchStart: this.scrollResponderHandleTouchStart,
|
|
||||||
onTouchMove: this.scrollResponderHandleTouchMove,
|
|
||||||
onTouchEnd: this.scrollResponderHandleTouchEnd,
|
|
||||||
onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag,
|
|
||||||
onScrollEndDrag: this.scrollResponderHandleScrollEndDrag,
|
|
||||||
onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin,
|
|
||||||
onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd,
|
|
||||||
onStartShouldSetResponder: this.scrollResponderHandleStartShouldSetResponder,
|
|
||||||
onStartShouldSetResponderCapture: this.scrollResponderHandleStartShouldSetResponderCapture,
|
|
||||||
onScrollShouldSetResponder: this.scrollResponderHandleScrollShouldSetResponder,
|
|
||||||
onResponderGrant: this.scrollResponderHandleResponderGrant,
|
|
||||||
onResponderRelease: this.scrollResponderHandleResponderRelease,
|
|
||||||
onResponderReject: this.scrollResponderHandleResponderReject,
|
|
||||||
onScroll: this.scrollResponderHandleScroll,
|
|
||||||
ref: INNERVIEW,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.props.onContentSizeChange) {
|
|
||||||
recyclerProps.onContentSizeChange = this._handleContentSizeChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
var wrappedChildren = React.Children.map(this.props.children, (child) => {
|
|
||||||
if (!child) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
collapsable={false}
|
|
||||||
style={styles.absolute}>
|
|
||||||
{child}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const refreshControl = this.props.refreshControl;
|
|
||||||
if (refreshControl) {
|
|
||||||
// Wrap the NativeAndroidRecyclerView with a AndroidSwipeRefreshLayout.
|
|
||||||
return React.cloneElement(
|
|
||||||
refreshControl,
|
|
||||||
{style: [styles.base, this.props.style]},
|
|
||||||
<NativeAndroidRecyclerView {...recyclerProps} style={styles.base}>
|
|
||||||
{wrappedChildren}
|
|
||||||
</NativeAndroidRecyclerView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NativeAndroidRecyclerView {...recyclerProps} style={[styles.base, this.props.style]}>
|
|
||||||
{wrappedChildren}
|
|
||||||
</NativeAndroidRecyclerView>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var styles = StyleSheet.create({
|
|
||||||
absolute: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
},
|
|
||||||
base: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var NativeAndroidRecyclerView = requireNativeComponent(
|
|
||||||
'AndroidRecyclerViewBackedScrollView',
|
|
||||||
RecyclerViewBackedScrollView
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = RecyclerViewBackedScrollView;
|
|
|
@ -1,8 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
|
||||||
*
|
|
||||||
* @providesModule RecyclerViewBackedScrollView
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = require('ScrollView');
|
|
|
@ -50,7 +50,6 @@ const ReactNative = {
|
||||||
get Slider() { return require('Slider'); },
|
get Slider() { return require('Slider'); },
|
||||||
get SnapshotViewIOS() { return require('SnapshotViewIOS'); },
|
get SnapshotViewIOS() { return require('SnapshotViewIOS'); },
|
||||||
get Switch() { return require('Switch'); },
|
get Switch() { return require('Switch'); },
|
||||||
get RecyclerViewBackedScrollView() { return require('RecyclerViewBackedScrollView'); },
|
|
||||||
get RefreshControl() { return require('RefreshControl'); },
|
get RefreshControl() { return require('RefreshControl'); },
|
||||||
get StatusBar() { return require('StatusBar'); },
|
get StatusBar() { return require('StatusBar'); },
|
||||||
get SwipeableListView() { return require('SwipeableListView'); },
|
get SwipeableListView() { return require('SwipeableListView'); },
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
include_defs('//ReactAndroid/DEFS')
|
include_defs('//ReactAndroid/DEFS')
|
||||||
|
|
||||||
deps = [
|
deps = [
|
||||||
|
'//java/com/facebook/fbreact/views/recyclerview:recyclerview',
|
||||||
react_native_dep('third-party/android/support/v4:lib-support-v4'),
|
react_native_dep('third-party/android/support/v4:lib-support-v4'),
|
||||||
react_native_dep('third-party/java/jsr-305:jsr-305'),
|
react_native_dep('third-party/java/jsr-305:jsr-305'),
|
||||||
react_native_dep('third-party/java/junit:junit'),
|
react_native_dep('third-party/java/junit:junit'),
|
||||||
|
@ -19,7 +20,6 @@ deps = [
|
||||||
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
|
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
|
||||||
react_native_target('java/com/facebook/react/views/picker:picker'),
|
react_native_target('java/com/facebook/react/views/picker:picker'),
|
||||||
react_native_target('java/com/facebook/react/views/progressbar:progressbar'),
|
react_native_target('java/com/facebook/react/views/progressbar:progressbar'),
|
||||||
react_native_target('java/com/facebook/react/views/recyclerview:recyclerview'),
|
|
||||||
react_native_target('java/com/facebook/react/views/scroll:scroll'),
|
react_native_target('java/com/facebook/react/views/scroll:scroll'),
|
||||||
react_native_target('java/com/facebook/react/views/slider:slider'),
|
react_native_target('java/com/facebook/react/views/slider:slider'),
|
||||||
react_native_target('java/com/facebook/react/views/swiperefresh:swiperefresh'),
|
react_native_target('java/com/facebook/react/views/swiperefresh:swiperefresh'),
|
||||||
|
|
|
@ -4,6 +4,7 @@ android_library(
|
||||||
name = 'shell',
|
name = 'shell',
|
||||||
srcs = glob(['**/*.java']),
|
srcs = glob(['**/*.java']),
|
||||||
deps = [
|
deps = [
|
||||||
|
'//java/com/facebook/fbreact/views/recyclerview:recyclerview',
|
||||||
react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'),
|
react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'),
|
||||||
react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'),
|
react_native_dep('libraries/soloader/java/com/facebook/soloader:soloader'),
|
||||||
react_native_dep('third-party/android/support/v4:lib-support-v4'),
|
react_native_dep('third-party/android/support/v4:lib-support-v4'),
|
||||||
|
@ -45,7 +46,6 @@ android_library(
|
||||||
react_native_target('java/com/facebook/react/views/modal:modal'),
|
react_native_target('java/com/facebook/react/views/modal:modal'),
|
||||||
react_native_target('java/com/facebook/react/views/picker:picker'),
|
react_native_target('java/com/facebook/react/views/picker:picker'),
|
||||||
react_native_target('java/com/facebook/react/views/progressbar:progressbar'),
|
react_native_target('java/com/facebook/react/views/progressbar:progressbar'),
|
||||||
react_native_target('java/com/facebook/react/views/recyclerview:recyclerview'),
|
|
||||||
react_native_target('java/com/facebook/react/views/scroll:scroll'),
|
react_native_target('java/com/facebook/react/views/scroll:scroll'),
|
||||||
react_native_target('java/com/facebook/react/views/slider:slider'),
|
react_native_target('java/com/facebook/react/views/slider:slider'),
|
||||||
react_native_target('java/com/facebook/react/views/swiperefresh:swiperefresh'),
|
react_native_target('java/com/facebook/react/views/swiperefresh:swiperefresh'),
|
||||||
|
|
|
@ -67,7 +67,6 @@ import com.facebook.react.views.modal.ReactModalHostManager;
|
||||||
import com.facebook.react.views.picker.ReactDialogPickerManager;
|
import com.facebook.react.views.picker.ReactDialogPickerManager;
|
||||||
import com.facebook.react.views.picker.ReactDropdownPickerManager;
|
import com.facebook.react.views.picker.ReactDropdownPickerManager;
|
||||||
import com.facebook.react.views.progressbar.ReactProgressBarViewManager;
|
import com.facebook.react.views.progressbar.ReactProgressBarViewManager;
|
||||||
import com.facebook.react.views.recyclerview.RecyclerViewBackedScrollViewManager;
|
|
||||||
import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager;
|
import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager;
|
||||||
import com.facebook.react.views.scroll.ReactScrollViewManager;
|
import com.facebook.react.views.scroll.ReactScrollViewManager;
|
||||||
import com.facebook.react.views.slider.ReactSliderManager;
|
import com.facebook.react.views.slider.ReactSliderManager;
|
||||||
|
@ -275,7 +274,6 @@ public class MainReactPackage extends LazyReactPackage {
|
||||||
viewManagers.add(new ReactViewPagerManager());
|
viewManagers.add(new ReactViewPagerManager());
|
||||||
viewManagers.add(new ReactVirtualTextViewManager());
|
viewManagers.add(new ReactVirtualTextViewManager());
|
||||||
viewManagers.add(new ReactWebViewManager());
|
viewManagers.add(new ReactWebViewManager());
|
||||||
viewManagers.add(new RecyclerViewBackedScrollViewManager());
|
|
||||||
viewManagers.add(new SwipeRefreshLayoutManager());
|
viewManagers.add(new SwipeRefreshLayoutManager());
|
||||||
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(reactContext);
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(reactContext);
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
include_defs('//ReactAndroid/DEFS')
|
|
||||||
|
|
||||||
android_library(
|
|
||||||
name = 'recyclerview',
|
|
||||||
srcs = glob(['**/*.java']),
|
|
||||||
deps = [
|
|
||||||
react_native_dep('third-party/android/support/v4:lib-support-v4'),
|
|
||||||
react_native_dep('third-party/android/support/v7/recyclerview:recyclerview'),
|
|
||||||
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
|
|
||||||
react_native_dep('third-party/java/jsr-305:jsr-305'),
|
|
||||||
react_native_target('java/com/facebook/react/bridge:bridge'),
|
|
||||||
react_native_target('java/com/facebook/react/common:common'),
|
|
||||||
react_native_target('java/com/facebook/react/touch:touch'),
|
|
||||||
react_native_target('java/com/facebook/react/uimanager:uimanager'),
|
|
||||||
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
|
|
||||||
react_native_target('java/com/facebook/react/views/scroll:scroll'),
|
|
||||||
react_native_target('java/com/facebook/react/views/view:view'),
|
|
||||||
],
|
|
||||||
visibility = [
|
|
||||||
'PUBLIC',
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
|
||||||
|
|
||||||
package com.facebook.react.views.recyclerview;
|
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.support.v7.widget.SimpleItemAnimator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of {@link RecyclerView.ItemAnimator} that disables all default animations.
|
|
||||||
*/
|
|
||||||
/*package*/ class NotAnimatedItemAnimator extends SimpleItemAnimator {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void runPendingAnimations() {
|
|
||||||
// nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateRemove(RecyclerView.ViewHolder holder) {
|
|
||||||
dispatchRemoveStarting(holder);
|
|
||||||
dispatchRemoveFinished(holder);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateAdd(RecyclerView.ViewHolder holder) {
|
|
||||||
dispatchAddStarting(holder);
|
|
||||||
dispatchAddFinished(holder);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateMove(
|
|
||||||
RecyclerView.ViewHolder holder,
|
|
||||||
int fromX,
|
|
||||||
int fromY,
|
|
||||||
int toX,
|
|
||||||
int toY) {
|
|
||||||
dispatchMoveStarting(holder);
|
|
||||||
dispatchMoveFinished(holder);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateChange(
|
|
||||||
RecyclerView.ViewHolder oldHolder,
|
|
||||||
RecyclerView.ViewHolder newHolder,
|
|
||||||
int fromLeft,
|
|
||||||
int fromTop,
|
|
||||||
int toLeft,
|
|
||||||
int toTop) {
|
|
||||||
dispatchChangeStarting(oldHolder, true);
|
|
||||||
dispatchChangeFinished(oldHolder, true);
|
|
||||||
dispatchChangeStarting(newHolder, false);
|
|
||||||
dispatchChangeFinished(newHolder, false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void endAnimation(RecyclerView.ViewHolder item) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void endAnimations() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRunning() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,397 +0,0 @@
|
||||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
|
||||||
|
|
||||||
package com.facebook.react.views.recyclerview;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import com.facebook.infer.annotation.Assertions;
|
|
||||||
import com.facebook.react.bridge.ReactContext;
|
|
||||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
|
||||||
import com.facebook.react.uimanager.UIManagerModule;
|
|
||||||
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
|
|
||||||
import com.facebook.react.uimanager.events.NativeGestureUtil;
|
|
||||||
import com.facebook.react.views.scroll.ScrollEvent;
|
|
||||||
import com.facebook.react.views.scroll.ScrollEventType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps {@link RecyclerView} providing interface similar to `ScrollView.js` where each children
|
|
||||||
* will be rendered as a separate {@link RecyclerView} row.
|
|
||||||
*
|
|
||||||
* Currently supports only vertically positioned item. Views will not be automatically recycled but
|
|
||||||
* they will be detached from native view hierarchy when scrolled offscreen.
|
|
||||||
*
|
|
||||||
* It works by storing all child views in an array within adapter and binding appropriate views to
|
|
||||||
* rows when requested.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
public class RecyclerViewBackedScrollView extends RecyclerView {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple implementation of {@link ViewHolder} as it's an abstract class. The only thing we need
|
|
||||||
* to hold in this implementation is the reference to {@link RecyclableWrapperViewGroup} that
|
|
||||||
* is already stored by default.
|
|
||||||
*/
|
|
||||||
private static class ConcreteViewHolder extends ViewHolder {
|
|
||||||
public ConcreteViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View that is going to be used as a cell in {@link RecyclerView}. It's going to be reusable and
|
|
||||||
* we will remove/attach views for a certain positions based on the {@code mViews} array stored
|
|
||||||
* in the adapter class.
|
|
||||||
*
|
|
||||||
* This method overrides {@link #onMeasure} and delegates measurements to the child view that has
|
|
||||||
* been attached to. This is because instances of {@link RecyclableWrapperViewGroup} are created
|
|
||||||
* outside of {@link NativeViewHierarchyManager} and their layout is not managed by that manager
|
|
||||||
* as opposed to all the other react-native views. Instead we use dimensions of the child view
|
|
||||||
* (dimensions has been set in layouting process) so that size of this view match the size of
|
|
||||||
* the view it wraps.
|
|
||||||
*/
|
|
||||||
private static class RecyclableWrapperViewGroup extends ViewGroup {
|
|
||||||
|
|
||||||
public RecyclableWrapperViewGroup(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
||||||
// This view will only have one child that is managed by the `NativeViewHierarchyManager` and
|
|
||||||
// its position and dimensions are set separately. We don't need to handle its layouting here
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
||||||
if (getChildCount() > 0) {
|
|
||||||
// We override measure spec and use dimensions of the children. Children is a view added
|
|
||||||
// from the adapter and always have a correct dimensions specified as they are calculated
|
|
||||||
// and set with NativeViewHierarchyManager
|
|
||||||
View child = getChildAt(0);
|
|
||||||
setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
|
|
||||||
} else {
|
|
||||||
Assertions.assertUnreachable("RecyclableWrapperView measured but no view attached");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
// This method is frequently called from the "onScroll" handler of the "RecyclerView" with an
|
|
||||||
// index of first visible item of the view. Implementation of this method takes advantage of
|
|
||||||
// that fact by caching the value for the last index that this method has been called with.
|
|
||||||
//
|
|
||||||
// There are a 2 cases that we optimize for:
|
|
||||||
// 1) The visible item doesn't change between subsequent "onScroll" calls, in that case we
|
|
||||||
// don't need to calculate anything, just return the cached value
|
|
||||||
// 2) The next visible item will be the one that is adjacent to the item that we store the
|
|
||||||
// cached value for: index + 1 when scrolling down or index - 1 when scrolling up. Then it
|
|
||||||
// is sufficient to add/subtract height of item at the "last index"
|
|
||||||
//
|
|
||||||
// The implementation accounts for the cases when next index is not necessarily a subsequent
|
|
||||||
// number of the cached one. In which case we try to minimize the number of rows we will loop
|
|
||||||
// through.
|
|
||||||
if (mLastRequestedPosition != index) {
|
|
||||||
int sum;
|
|
||||||
|
|
||||||
if (mLastRequestedPosition < index) {
|
|
||||||
// This can either happen when we're scrolling down or if the cached value has never been
|
|
||||||
// calculated
|
|
||||||
int startIndex;
|
|
||||||
|
|
||||||
if (mLastRequestedPosition != -1) {
|
|
||||||
// We already have the value cached, let's use it and only add heights of the items
|
|
||||||
// starting at the index we have the cached value for
|
|
||||||
sum = mOffsetForLastPosition;
|
|
||||||
startIndex = mLastRequestedPosition;
|
|
||||||
} else {
|
|
||||||
sum = 0;
|
|
||||||
startIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = startIndex; i < index; i++) {
|
|
||||||
sum += mReactListAdapter.mViews.get(i).getMeasuredHeight();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We are scrolling up, we can either use cached value and subtract heights of rows
|
|
||||||
// between mLastRequestPosition and index, or we can calculate the height starting from 0
|
|
||||||
// (this can be quite a frequent case as well, when the list implements "jump to the top"
|
|
||||||
// action). We just go for the option that require less calculations
|
|
||||||
if (index < (mLastRequestedPosition - index)) {
|
|
||||||
// index is relatively small, it's faster to calculate the sum starting from 0
|
|
||||||
sum = 0;
|
|
||||||
for (int i = 0; i < index; i++) {
|
|
||||||
sum += mReactListAdapter.mViews.get(i).getMeasuredHeight();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// index is "closer" to the last cached index than it is to 0. We can reuse cached sum
|
|
||||||
// and calculate the new sum by subtracting heights of the elements between
|
|
||||||
// "mLastRequestPosition" and "index"
|
|
||||||
sum = mOffsetForLastPosition;
|
|
||||||
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 ScrollOffsetTracker mScrollOffsetTracker;
|
|
||||||
private final RecyclerViewBackedScrollView mScrollView;
|
|
||||||
private int mTotalChildrenHeight = 0;
|
|
||||||
|
|
||||||
// The following `OnLayoutChangeListsner` is attached to the views stored in the adapter
|
|
||||||
// `mViews` array. It's used to get layout information passed to that view from css-layout
|
|
||||||
// and to update its layout to be enclosed in the wrapper view group.
|
|
||||||
private final View.OnLayoutChangeListener
|
|
||||||
mChildLayoutChangeListener = new View.OnLayoutChangeListener() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLayoutChange(
|
|
||||||
View v,
|
|
||||||
int left,
|
|
||||||
int top,
|
|
||||||
int right,
|
|
||||||
int bottom,
|
|
||||||
int oldLeft,
|
|
||||||
int oldTop,
|
|
||||||
int oldRight,
|
|
||||||
int oldBottom) {
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
if (oldHeight != newHeight) {
|
|
||||||
updateTotalChildrenHeight(newHeight - oldHeight);
|
|
||||||
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
|
|
||||||
// be updated if the wrapped view dimensions are changed.
|
|
||||||
// To achieve that we call `forceLayout()` on the view modified and on `RecyclerView`
|
|
||||||
// instance (which is accessible with `v.getParent().getParent()` if the view is
|
|
||||||
// attached). We rely on NativeViewHierarchyManager to call `layout` on `RecyclerView`
|
|
||||||
// then, which will happen once all the children of `RecyclerView` have their layout
|
|
||||||
// updated. This will trigger `layout` call on attached wrapper nodes and will let us
|
|
||||||
// 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 (v.getParent() != null
|
|
||||||
&& v.getParent().getParent() != null) {
|
|
||||||
View wrapper = (View) v.getParent(); // native view that wraps view added to adapter
|
|
||||||
wrapper.forceLayout();
|
|
||||||
// wrapper.getParent() points to the recycler if the view is currently attached (it
|
|
||||||
// could be in "scrape" state when it is attached to recyclable wrapper but not to
|
|
||||||
// the recycler)
|
|
||||||
((View) wrapper.getParent()).forceLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public ReactListAdapter(RecyclerViewBackedScrollView scrollView) {
|
|
||||||
mScrollView = scrollView;
|
|
||||||
mScrollOffsetTracker = new ScrollOffsetTracker(this);
|
|
||||||
setHasStableIds(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addView(View child, int index) {
|
|
||||||
mViews.add(index, child);
|
|
||||||
|
|
||||||
updateTotalChildrenHeight(child.getMeasuredHeight());
|
|
||||||
child.addOnLayoutChangeListener(mChildLayoutChangeListener);
|
|
||||||
|
|
||||||
notifyItemInserted(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeViewAt(int index) {
|
|
||||||
View child = mViews.get(index);
|
|
||||||
if (child != null) {
|
|
||||||
mViews.remove(index);
|
|
||||||
child.removeOnLayoutChangeListener(mChildLayoutChangeListener);
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(ConcreteViewHolder holder, int position) {
|
|
||||||
RecyclableWrapperViewGroup vg = (RecyclableWrapperViewGroup) holder.itemView;
|
|
||||||
View row = mViews.get(position);
|
|
||||||
if (row.getParent() != vg) {
|
|
||||||
vg.addView(row, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewRecycled(ConcreteViewHolder holder) {
|
|
||||||
super.onViewRecycled(holder);
|
|
||||||
((RecyclableWrapperViewGroup) holder.itemView).removeAllViews();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return mViews.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
return mViews.get(position).getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public View getView(int index) {
|
|
||||||
return mViews.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalChildrenHeight() {
|
|
||||||
return mTotalChildrenHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTopOffsetForItem(int index) {
|
|
||||||
return mScrollOffsetTracker.getTopOffsetForItem(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean mSendContentSizeChangeEvents;
|
|
||||||
|
|
||||||
public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
|
|
||||||
mSendContentSizeChangeEvents = sendContentSizeChangeEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int calculateAbsoluteOffset() {
|
|
||||||
int offsetY = 0;
|
|
||||||
if (getChildCount() > 0) {
|
|
||||||
View recyclerViewChild = getChildAt(0);
|
|
||||||
int childPosition = getChildViewHolder(recyclerViewChild).getLayoutPosition();
|
|
||||||
offsetY = ((ReactListAdapter) getAdapter()).getTopOffsetForItem(childPosition) -
|
|
||||||
recyclerViewChild.getTop();
|
|
||||||
}
|
|
||||||
return offsetY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*package*/ void scrollTo(int scrollX, int scrollY, boolean animated) {
|
|
||||||
int deltaY = scrollY - calculateAbsoluteOffset();
|
|
||||||
if (animated) {
|
|
||||||
smoothScrollBy(0, deltaY);
|
|
||||||
} else {
|
|
||||||
scrollBy(0, deltaY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
|
|
||||||
super.onScrollChanged(l, t, oldl, oldt);
|
|
||||||
|
|
||||||
((ReactContext) getContext()).getNativeModule(UIManagerModule.class).getEventDispatcher()
|
|
||||||
.dispatchEvent(ScrollEvent.obtain(
|
|
||||||
getId(),
|
|
||||||
ScrollEventType.SCROLL,
|
|
||||||
0, /* offsetX = 0, horizontal scrolling only */
|
|
||||||
calculateAbsoluteOffset(),
|
|
||||||
getWidth(),
|
|
||||||
((ReactListAdapter) getAdapter()).getTotalChildrenHeight(),
|
|
||||||
getWidth(),
|
|
||||||
getHeight()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onTotalChildrenHeightChange(int newTotalChildrenHeight) {
|
|
||||||
if (mSendContentSizeChangeEvents) {
|
|
||||||
((ReactContext) getContext()).getNativeModule(UIManagerModule.class).getEventDispatcher()
|
|
||||||
.dispatchEvent(new ContentSizeChangeEvent(
|
|
||||||
getId(),
|
|
||||||
getWidth(),
|
|
||||||
newTotalChildrenHeight));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public RecyclerViewBackedScrollView(Context context) {
|
|
||||||
super(context);
|
|
||||||
setHasFixedSize(true);
|
|
||||||
setItemAnimator(new NotAnimatedItemAnimator());
|
|
||||||
setLayoutManager(new LinearLayoutManager(context));
|
|
||||||
setAdapter(new ReactListAdapter(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*package*/ void addViewToAdapter(View child, int index) {
|
|
||||||
((ReactListAdapter) getAdapter()).addView(child, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*package*/ void removeViewFromAdapter(int index) {
|
|
||||||
((ReactListAdapter) getAdapter()).removeViewAt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*package*/ View getChildAtFromAdapter(int index) {
|
|
||||||
return ((ReactListAdapter) getAdapter()).getView(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*package*/ int getChildCountFromAdapter() {
|
|
||||||
return getAdapter().getItemCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
||||||
if (super.onInterceptTouchEvent(ev)) {
|
|
||||||
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
|
||||||
|
|
||||||
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.annotations.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.ScrollEventType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View manager for {@link RecyclerViewBackedScrollView}.
|
|
||||||
*/
|
|
||||||
public class RecyclerViewBackedScrollViewManager extends
|
|
||||||
ViewGroupManager<RecyclerViewBackedScrollView>
|
|
||||||
implements ReactScrollViewCommandHelper.ScrollCommandHandler<RecyclerViewBackedScrollView> {
|
|
||||||
|
|
||||||
private static final String REACT_CLASS = "AndroidRecyclerViewBackedScrollView";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return REACT_CLASS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addView(RecyclerViewBackedScrollView parent, View child, int index) {
|
|
||||||
parent.addViewToAdapter(child, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getChildCount(RecyclerViewBackedScrollView parent) {
|
|
||||||
return parent.getChildCountFromAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getChildAt(RecyclerViewBackedScrollView parent, int index) {
|
|
||||||
return parent.getChildAtFromAdapter(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeViewAt(RecyclerViewBackedScrollView parent, int index) {
|
|
||||||
parent.removeViewFromAdapter(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides implementation of commands supported by {@link ReactScrollViewManager}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void receiveCommand(
|
|
||||||
RecyclerViewBackedScrollView view,
|
|
||||||
int commandId,
|
|
||||||
@Nullable ReadableArray args) {
|
|
||||||
ReactScrollViewCommandHelper.receiveCommand(this, view, commandId, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void scrollTo(
|
|
||||||
RecyclerViewBackedScrollView scrollView,
|
|
||||||
ReactScrollViewCommandHelper.ScrollToCommandData data) {
|
|
||||||
scrollView.scrollTo(data.mDestX, data.mDestY, data.mAnimated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void scrollToEnd(
|
|
||||||
RecyclerViewBackedScrollView scrollView,
|
|
||||||
ReactScrollViewCommandHelper.ScrollToEndCommandData data) {
|
|
||||||
// Not implemented.
|
|
||||||
// RecyclerViewBackedScrollView is deprecated and will be removed.
|
|
||||||
// People should use a standard ScrollView or ListView instead.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable
|
|
||||||
Map getExportedCustomDirectEventTypeConstants() {
|
|
||||||
return MapBuilder.builder()
|
|
||||||
.put(ScrollEventType.SCROLL.getJSEventName(), MapBuilder.of("registrationName", "onScroll"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue