diff --git a/packages/rax-recyclerview/CHANGELOG.md b/packages/rax-recyclerview/CHANGELOG.md
index 3723c0cb..2e8bb85b 100644
--- a/packages/rax-recyclerview/CHANGELOG.md
+++ b/packages/rax-recyclerview/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## 2.0.0
+
+- Refactor `rax-recyclerview` in web and runtime miniapp:
+ - Replace `rax-view` with `rax-scrollview`;
+ - Update placeholder boxes size instead of updating item style: top or left ;
+- upgrade deps: `rax-scrollview` from `^3.3.3` to `^3.6.0`
+
## 1.3.6
- Fix RecyclerView error: "Uncaught TypeError: Cannot add property style, object is not extensible", when use virtual list mode.
diff --git a/packages/rax-recyclerview/README.md b/packages/rax-recyclerview/README.md
index 5932379f..218eaff9 100644
--- a/packages/rax-recyclerview/README.md
+++ b/packages/rax-recyclerview/README.md
@@ -4,7 +4,9 @@
## 描述
-`ScrollView` 的同门师兄,在 Weex 下是对 `list` 与 `cell` 的包装,其具有复用内部组件来提供性能的机制。
+在 `ScrollView` 的基础上,按需渲染视图内的元素,并回收视图外元素,以优化长列表场景下的性能优化。在 Weex 下是对 `list` 与 `cell` 的包装,其具有复用内部组件来提供性能的机制。
+
+使用时,如果为垂直方向滚动时,**必须设置高度**,否则元素回收功能和滚动相关的功能将失效。

@@ -14,39 +16,92 @@
npm install rax-recyclerview --save
```
+## 属性
+在 rax-recyclerview@2.0.0 及以上版本中,可以使用所有 rax-scrollview 的所有属性,具体文档请看:[rax-scrollview](https://github.com/raxjs/rax-components/blob/master/packages/rax-scrollview/README.md)
-## 属性
+| 属性 | 类型 | 默认值 | 必填 | 描述 | 支持 |
+| --------------------- | ----------------- | ------ | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| itemSize | `function/number` | - | × | 单位为`rpx`, 返回每个子元素的高度(节点回收时需要,不需要传递 RecyclerView.Header 元素的高度),若 itemSize 未传,则不进行回收。 如果每个子元素的高度不固定,则可使用函数的方式,例如 itemSize={(index) => {return 100;}} 其中 `index` 为参与回收的子元素的数组下标位置 | 

|
+| itemEstimateSize | `number` | - | × | 单位为`rpx`, 当子元素不是固定高度时,可以传入该值作为元素的估计值,本属性只支持 Web |
|
+| horizontal | `boolean` | - | false | 设置为横向滚动 |


|
+| onEndReachedThreshold | `string/number` | 500 | ✘ | 设置加载更多的偏移, 推荐使用 string 格式来指指定尺寸单位,如`100rpx` |

|
+| onEndReached | `function` | - | ✘ | 滚动区域还剩`onEndReachedThreshold`的长度时触发 |

|
+| onScroll | `function` | - | ✘ | 滚动时触发的事件,返回当前滚动的水平垂直距离 |

|
+| bufferSize | `number` | - | ✘ | 缓冲区单位尺寸,单位为 rpx,默认为当前视图的尺寸(水如水平方向滚动时,值为 750), recyclerview 会默认渲染 3 屏缓冲区尺寸。注意在小程序中,慎用该属性! | 

|
+| onTouchStart | `function` | - | false | touchStart 触发的事件,返回触摸点数据(touches、changedTouches) |

|
+| onTouchMove | `function` | - | false | touchMove 触发的事件,返回触摸点数据(touches、changedTouches) |

|
+| onTouchEnd | `function` | - | false | touchEnd 触发的事件,返回触摸点数据(touches、changedTouches) |

|
+| onTouchCancel | `function` | - | false | touchCancel 触发的事件,返回触摸点数据(touches、changedTouches) |
|
+| disableScroll | `boolean` | - | false | 是否禁止滚动,是否禁止滚动, rax-recyclerview@1.3.4 及以上版本支 |
|
+
+## 子组件
+
+### RecyclerView.Header
+
+头部子元素需要用 `RecycerView.Header` 包裹,头部元素**不参与**元素回收。
-| 属性 | 类型 | 默认值 | 必填 | 描述 | 支持 |
-| --------------------- | ----------------- | ------ | ---- | ----------------------------------------------- | ------------------------------------------------------------ |
-| onEndReachedThreshold | `number` | 500 | ✘ | 设置加载更多的偏移 |

|
-| onEndReached | `function` | - | ✘ | 滚动区域还剩`onEndReachedThreshold`的长度时触发 |

|
-| onScroll | `function` | - | ✘ | 滚动时触发的事件,返回当前滚动的水平垂直距离 |

|
-| itemSize | `function/number` | - | ✘ | 返回每个 cell 的高度(节点回收时需要) |
|
-| totalSize | `number` | - | ✘ | 当前列表总高度(在 cell 高度可变的列表中需要传) |
|
-| onTouchStart | `function` | - | false | touchStart触发的事件,返回触摸点数据(touches、changedTouches) |

|
-| onTouchMove | `function` | - | false | touchMove触发的事件,返回触摸点数据(touches、changedTouches) |

|
-| onTouchEnd | `function` | - | false | touchEnd触发的事件,返回触摸点数据(touches、changedTouches) |

|
-| onTouchCancel | `function` | - | false | touchCancel触发的事件,返回触摸点数据(touches、changedTouches) |
|
-| disableScroll | `boolean` | - | false | 是否禁止滚动,是否禁止滚动, rax-recyclerview@1.3.4 及以上版本支 |
|
+### RecyclerView.Cell
+
+除了头部元素之外的子元素可以被 `RecyclerView.Cell` 包裹,在 Weex 中该组件为 Weex 的 `cell` 组件,在 Web 和小程序中该组件是 `Fragment` 空节点。该节点没有实际意义,所以不要在该组件上设置样式和绑定事件。如果在 Web 和 小程序中使用,不需要包裹 `RecyclerView.Cell`。
## 方法
-### scrollTo({x:number,y:number})
+
+### scrollTo({x:number|string,y:number|string})
#### 参数
参数为 `object`,包含以下属性
-| **属性** | **类型** | **默认值** | **必填** | **描述** |
-| -------- | -------- | ---------- | -------- | ------------ |
-| x | `number` | - | ✘ | 横向的偏移量 |
-| y | `number` | - | ✘ | 纵向的偏移量 |
+| **属性** | **类型** | **默认值** | **必填** | **描述** |
+| -------- | --------------- | ---------- | -------- | ------------------------------------------------ |
+| x | `number/string` | - | ✘ | 横向的偏移量, 推荐使用 string 格式来指定尺寸单位 |
+| y | `number/string` | - | ✘ | 纵向的偏移量, 推荐使用 string 格式来指定尺寸单位 |
+
+## 优化
+
+无论是在 web 还是在小程序中,节点的数量会影响页面的渲染性能,这也是 `rax-recyclerview` 组件通过回收视图外的节点来做到优化性能的原因。
+
+但是需要注意的是,`rax-recyclerview` 并不是万能的,相反,由于已经渲染的组件离开视图外之后会被回收,因此当其重新进入视图的时候需要再次渲染。如果组件中的节点比较复杂,也会再次导致渲染性能问题。
+
+特别在运行时小程序中,由于其原理性问题,这种情况会更加突出。
+
+这里介绍几种有效提升性能的方法:
+
+1. 请务必减少被回收区块的节点数量和层级,并避免使用内联样式等任何增加节点信息的用法。
+
+```jsx
+// 修改前
+
+
+
+ 名称:
+ {name}
+
+
+
+```
+
+例如以上片段,减少层级和数量之后:
+
+```jsx
+// 修改后
+
+ 名称:{name}
+
+```
+
+2. 减少 `rax-recyclerview` 组件所在的层级,在小程序中,`setData` 的 `path` 层级会影响到渲染效率,因此也应当减少组件所在的层级。
+
+3. 可以适当把可回收组件修改为编译时组件,方法可查看:[使用编译时组件](https://rax.alibaba-inc.com/docs/guide/use-miniapp-compile-components)。本方法不适用于:
+
+- 回收组件中使用了运行时组件的,例如 `fusion mobile`;
+- 回收组件接受的数据量较大;
### 示例
```jsx
-import { createElement, Component, render } from "rax";
+import { createElement, Component, render, useRef } from "rax";
import View from "rax-view";
@@ -56,162 +111,84 @@ import DriverUniversal from "driver-universal";
import RecyclerView from "rax-recyclerview";
-class Thumb extends Component {
- shouldComponentUpdate(nextProps, nextState) {
- return false;
- }
-
- render() {
- return (
-
-
-
-
-
- );
- }
-}
-
-class Row extends Component {
- handleClick = e => {
- this.props.onClick(this.props.data);
- };
-
- render() {
- return (
-
-
-
- {this.props.data.text + " (" + this.props.data.clicks + " clicks)"}
-
-
+function Thumb({ val }) {
+ return (
+
+
+ {val}
- );
- }
+
+ );
}
-
-const THUMBS = [];
-
-for (let i = 0; i < 20; i++) THUMBS.push(i);
-
-const createThumbRow = (val, i) => ;
-
-class App extends Component {
- state = {
- horizontalScrollViewEventLog: false,
- scrollViewEventLog: false
- };
-
- render() {
- return (
-
-
- {
- this.scrollView = scrollView;
- }}
- style={{
- height: 500
- }}
- onEndReached={() => alert("reach end")}
- >
-
- Sticky view is not header{" "}
-
-
-
- Sticky view must in header root
-
-
- {THUMBS.map(createThumbRow)}
-
- this.scrollView.scrollTo({ y: 0 })}
- >
- Scroll to top
-
-
- {this.state.scrollViewEventLog ? "onEndReached" : ""}
+let THUMBS = [];
+for (let i = 0; i < 100; i++) THUMBS.push(`box_${i}`);
+let createThumbRow = (val) => ;
+
+function App() {
+ const viewRef = useRef(null);
+
+ return (
+
+ {
+ console.log(e.nativeEvent.contentOffset.y);
+ }}
+ >
+
+ Sticky view is not header
+
+
+
+ Sticky view must in header root
-
+
+ {THUMBS.map(createThumbRow)}
+
+ viewRef.current.scrollTo({ y: 0 })}
+ >
+ Scroll to top
- );
- }
+
+ );
}
-const styles = {
+let styles = {
root: {
- width: 750,
- paddingTop: 20
+ display: "block",
},
-
sticky: {
position: "sticky",
width: 750,
- backgroundColor: "#cccccc"
+ top: 0,
+ backgroundColor: "#cccccc",
},
-
container: {
- padding: 20,
- borderStyle: "solid",
- borderColor: "#dddddd",
- borderWidth: 1,
- marginLeft: 20,
- height: 1000,
- marginRight: 20,
- marginBottom: 10
+ height: "100vh",
},
-
button: {
margin: 7,
padding: 5,
alignItems: "center",
backgroundColor: "#eaeaea",
- borderRadius: 3
+ borderRadius: 3,
},
-
box: {
width: 64,
- height: 64
- },
-
- eventLogBox: {
- padding: 10,
- margin: 10,
- height: 80,
- borderWidth: 1,
- borderColor: "#f0f0f0",
- backgroundColor: "#f9f9f9"
- },
-
- row: {
- borderColor: "grey",
- borderWidth: 1,
- padding: 20,
- margin: 5
+ height: 64,
},
-
- text: {
- alignSelf: "center",
- color: "black"
+ fixButton: {
+ position: "fixed",
+ bottom: 20,
+ right: 20,
+ border: 1,
+ backgroundColor: "#fff",
},
-
- refreshView: {
- height: 80,
- width: 750,
- justifyContent: "center",
- alignItems: "center"
- },
-
- refreshArrow: {
- fontSize: 30,
- color: "#45b5f0"
- }
};
render(, document.body, { driver: DriverUniversal });
-
```
-
-
diff --git a/packages/rax-recyclerview/demo/base.jsx b/packages/rax-recyclerview/demo/base.jsx
index 5be6c6a0..280e9003 100644
--- a/packages/rax-recyclerview/demo/base.jsx
+++ b/packages/rax-recyclerview/demo/base.jsx
@@ -1,44 +1,49 @@
-import { createElement, Component, render } from 'rax';
+import { createElement, render, useRef } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import DriverUniversal from "driver-universal"
import RecyclerView from 'rax-recyclerview';
-function Thumb() {
+
+function Thumb({val}) {
return (
-
+ {val}
);
}
let THUMBS = [];
-for (let i = 0; i < 20; i++) THUMBS.push(i);
-let createThumbRow = (val, i) => ;
+for (let i = 0; i < 100; i++) THUMBS.push(`box_${i}`);
+let createThumbRow = (val) => ;
function App() {
+ const viewRef = useRef(null);
+
return (
-
-
-
-
- Sticky view is not header
-
-
-
-
- Sticky view must in header root
-
-
-
- {THUMBS.map(createThumbRow)}
-
-
+ {console.log(e.nativeEvent.contentOffset.y)}}
+ >
+
+ Sticky view is not header
+
+
+
+ Sticky view must in header root
+
+
+ {THUMBS.map(createThumbRow)}
+
+ viewRef.current.scrollTo({ y: 0 })}
+ >
+ Scroll to top
);
@@ -46,8 +51,7 @@ function App() {
let styles = {
root: {
- width: 750,
- paddingTop: 20
+ display: 'block'
},
sticky: {
position: 'sticky',
@@ -56,14 +60,7 @@ let styles = {
backgroundColor: '#cccccc'
},
container: {
- padding: 20,
- borderStyle: 'solid',
- borderColor: '#dddddd',
- borderWidth: 1,
- marginLeft: 20,
- height: 1000,
- marginRight: 20,
- marginBottom: 10,
+ height: '100vh'
},
button: {
margin: 7,
@@ -75,6 +72,13 @@ let styles = {
box: {
width: 64,
height: 64,
+ },
+ fixButton: {
+ position: 'fixed',
+ bottom: 20,
+ right: 20,
+ border: 1,
+ backgroundColor: '#fff'
}
};
diff --git a/packages/rax-recyclerview/demo/index.md b/packages/rax-recyclerview/demo/index.md
index ad17ea01..80b092b5 100644
--- a/packages/rax-recyclerview/demo/index.md
+++ b/packages/rax-recyclerview/demo/index.md
@@ -7,14 +7,15 @@ basic usage
```jsx
-import {createElement, Component, render} from 'rax';
-import DriverUniversal from 'driver-universal';
-import View from 'rax-view';
-import Text from 'rax-text';
-import {isWeex} from 'universal-env';
-import RecyclerView from '../src/index';
+import { createElement, Component, render } from "rax";
-const vwh = isWeex ? 667 * 2 : document.documentElement.clientHeight * 750 / document.documentElement.clientWidth;
+import View from "rax-view";
+
+import Text from "rax-text";
+
+import DriverUniversal from "driver-universal";
+
+import RecyclerView from "rax-recyclerview";
class Thumb extends Component {
shouldComponentUpdate(nextProps, nextState) {
@@ -23,25 +24,43 @@ class Thumb extends Component {
render() {
return (
-
+
-
- {this.props.index}
-
+
);
}
}
-let THUMBS = [];
-for (let i = 0; i < 30; i++) THUMBS.push(i);
-let createThumbRow = (val, i) => ;
+class Row extends Component {
+ handleClick = e => {
+ this.props.onClick(this.props.data);
+ };
-export default class App extends Component {
+ render() {
+ return (
+
+
+
+ {this.props.data.text + " (" + this.props.data.clicks + " clicks)"}
+
+
+
+ );
+ }
+}
+
+const THUMBS = [];
+
+for (let i = 0; i < 20; i++) THUMBS.push(i);
+
+const createThumbRow = (val, i) => ;
+
+class App extends Component {
state = {
horizontalScrollViewEventLog: false,
- scrollViewEventLog: false,
+ scrollViewEventLog: false
};
render() {
@@ -49,25 +68,32 @@ export default class App extends Component {
{
+ ref={scrollView => {
this.scrollView = scrollView;
}}
style={{
- height: vwh
+ height: 500
}}
- onEndReached={() => console.log('reach end')}>
-
- Simple Header
+ onEndReached={() => alert("reach end")}
+ >
+
+ Sticky view is not header{" "}
+
+
+
+ Sticky view must in header root
+
-
{THUMBS.map(createThumbRow)}
-
-
this.scrollView.scrollTo({y: 0})}>
- Top
+ style={styles.button}
+ onClick={() => this.scrollView.scrollTo({ y: 0 })}
+ >
+ Scroll to top
+
+
+ {this.state.scrollViewEventLog ? "onEndReached" : ""}
@@ -75,91 +101,73 @@ export default class App extends Component {
}
}
-let styles = {
+const styles = {
root: {
- width: 750
- },
- header: {
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
width: 750,
- height: 350,
- backgroundColor: 'tomato',
- },
- headerText: {
- color: 'white',
- fontSize: 40,
+ paddingTop: 20
},
+
sticky: {
- position: 'sticky',
+ position: "sticky",
width: 750,
- backgroundColor: '#cccccc'
+ backgroundColor: "#cccccc"
},
+
container: {
- borderStyle: 'solid',
- borderColor: '#dddddd',
- // borderWidth: 1,
- height: vwh,
- backgroundColor: '#eeeeee',
+ padding: 20,
+ borderStyle: "solid",
+ borderColor: "#dddddd",
+ borderWidth: 1,
+ marginLeft: 20,
+ height: 1000,
+ marginRight: 20,
+ marginBottom: 10
},
+
button: {
- backgroundColor: '#ffffff',
- width: 710,
- height: 250,
- marginTop: 20,
- marginLeft: 20,
- justifyContent: 'center',
- alignItems: 'center',
- borderRadius: 10,
+ margin: 7,
+ padding: 5,
+ alignItems: "center",
+ backgroundColor: "#eaeaea",
+ borderRadius: 3
},
+
box: {
width: 64,
- height: 64,
+ height: 64
},
+
eventLogBox: {
padding: 10,
margin: 10,
height: 80,
borderWidth: 1,
- borderColor: '#f0f0f0',
- backgroundColor: '#f9f9f9',
+ borderColor: "#f0f0f0",
+ backgroundColor: "#f9f9f9"
},
+
row: {
- borderColor: 'grey',
+ borderColor: "grey",
borderWidth: 1,
padding: 20,
- margin: 5,
+ margin: 5
},
+
text: {
- alignSelf: 'center',
- color: 'black',
+ alignSelf: "center",
+ color: "black"
},
+
refreshView: {
height: 80,
width: 750,
- justifyContent: 'center',
- alignItems: 'center'
+ justifyContent: "center",
+ alignItems: "center"
},
+
refreshArrow: {
fontSize: 30,
- color: '#45b5f0'
- },
- topIcon: {
- position: 'fixed',
- right: 40,
- bottom: 40,
- width: 100,
- height: 100,
- borderWidth: '1px',
- borderStyle: 'solid',
- borderColor: '#cccccc',
- borderRadius: 100,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: 'white',
- cursor: 'pointer',
+ color: "#45b5f0"
}
};
diff --git a/packages/rax-recyclerview/package.json b/packages/rax-recyclerview/package.json
index 6c669149..58e15a3d 100644
--- a/packages/rax-recyclerview/package.json
+++ b/packages/rax-recyclerview/package.json
@@ -1,11 +1,24 @@
{
"name": "rax-recyclerview",
- "version": "1.3.6",
+ "version": "2.0.0-12",
"description": "RecyclerView component for Rax.",
"license": "BSD-3-Clause",
"main": "lib/index.js",
"module": "es/index.js",
"types": "lib/index.d.ts",
+ "exports": {
+ ".": {
+ "web": "./es/web/index.js",
+ "weex": "./es/weex/index.js",
+ "miniapp": "./es/miniapp-runtime/index.js",
+ "wechat-miniprogram": "./es/miniapp-runtime/index.js",
+ "bytedance-microapp": "./es/miniapp-runtime/index.js",
+ "baidu-smartprogram": "./es/miniapp-runtime/index.js",
+ "kuaishou-miniprogram": "./es/miniapp-runtime/index.js",
+ "default": "./es/index.js"
+ },
+ "./*": "./*"
+ },
"miniprogram": ".",
"miniappConfig": {
"main": "lib/miniapp/index",
@@ -51,13 +64,15 @@
"react-component"
],
"dependencies": {
+ "@uni/env": "^1.0.7",
+ "@uni/intersection-observer": "^1.0.7",
"prop-types": "^15.7.2",
"rax-children": "^1.0.0",
- "rax-find-dom-node": "^1.0.0",
+ "rax-find-dom-node": "^1.0.1",
+ "rax-get-element-by-id": "^1.0.0",
"rax-refreshcontrol": "^1.0.0",
- "rax-scrollview": "^3.3.3",
- "rax-view": "^2.0.0",
- "universal-env": "^3.0.0"
+ "rax-scrollview": "3.7.1-1",
+ "rax-view": "^2.0.0"
},
"peerDependencies": {
"rax": "^1.0.0"
diff --git a/packages/rax-recyclerview/src/VirtualizedList/BaseList.js b/packages/rax-recyclerview/src/VirtualizedList/BaseList.js
deleted file mode 100644
index f417313b..00000000
--- a/packages/rax-recyclerview/src/VirtualizedList/BaseList.js
+++ /dev/null
@@ -1,306 +0,0 @@
-import { createElement, PureComponent } from 'rax';
-import PropTypes from 'prop-types';
-
-import SizeAndPositionManager from './SizeAndPositionManager';
-
-import {
- ALIGNMENT,
- DIRECTION,
- SCROLL_CHANGE_REASON,
- marginProp,
- oppositeMarginProp,
- positionProp,
- scrollProp,
- sizeProp,
-} from './constants';
-
-const DEFAULT_VIEWPORT = 750;
-
-const STYLE_ITEM = {
- position: 'absolute',
- top: 0,
- left: 0,
- width: '100%',
-};
-
-const STYLE_STICKY_ITEM = {
- ...STYLE_ITEM,
- position: 'sticky',
-};
-
-const STYLE_INNER = {
- position: 'relative',
- width: '100%',
- minHeight: '100%',
-};
-
-const STYLE_WRAPPER = {
- overflow: 'auto',
- willChange: 'transform',
- WebkitOverflowScrolling: 'touch',
-};
-
-export default class BaseList extends PureComponent {
- pixelRatio = 1;
- styleCache = {};
-
- static defaultProps = {
- overscanCount: 3,
- };
-
- static propTypes = {
- estimatedItemSize: PropTypes.number,
- itemSize: PropTypes.oneOfType([
- PropTypes.number,
- PropTypes.array,
- PropTypes.func,
- ]).isRequired,
- overscanCount: PropTypes.number,
- scrollOffset: PropTypes.number,
- scrollToIndex: PropTypes.number,
- scrollToAlignment: PropTypes.oneOf([
- ALIGNMENT.AUTO,
- ALIGNMENT.START,
- ALIGNMENT.CENTER,
- ALIGNMENT.END,
- ]),
- horizontal: PropTypes.bool,
- stickyIndices: PropTypes.arrayOf(PropTypes.number),
- style: PropTypes.object,
- width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- totalSize: PropTypes.number,
- };
-
- state = {
- offset:
- this.props.scrollOffset ||
- this.props.scrollToIndex != null &&
- this.getOffsetForIndex(this.props.scrollToIndex) ||
- 0,
- scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED,
- };
-
- pixelRatio = DEFAULT_VIEWPORT / this.getClientWidth();
-
- getEstimatedItemSize(props = this.props) {
- return (
- props.estimatedItemSize ||
- typeof props.itemSize === 'number' && props.itemSize ||
- 50
- );
- }
-
- getSize(index, itemSize) {
- if (typeof itemSize === 'function') {
- return itemSize(index);
- }
-
- return Array.isArray(itemSize) ? itemSize[index] : itemSize;
- }
-
- getStyle(index, sticky) {
- const style = this.styleCache[index];
-
- if (style) {
- return style;
- }
-
- const {
- size,
- offset,
- } = this.sizeAndPositionManager.getSizeAndPositionForIndex(index);
-
- return this.styleCache[index] = sticky ? {
- ...STYLE_STICKY_ITEM,
- [sizeProp[this.scrollDirection]]: size,
- [marginProp[this.scrollDirection]]: offset,
- [oppositeMarginProp[this.scrollDirection]]: -(offset + size),
- zIndex: 1,
- } : {
- ...STYLE_ITEM,
- [sizeProp[this.scrollDirection]]: size,
- [positionProp[this.scrollDirection]]: offset,
- };
- }
-
- itemSizeGetter = (itemSize) => {
- return index => this.getSize(index, itemSize);
- };
-
- sizeAndPositionManager = new SizeAndPositionManager({
- itemCount: this.props.children.length,
- itemSizeGetter: this.itemSizeGetter(this.props.itemSize),
- estimatedItemSize: this.getEstimatedItemSize(),
- });
- scrollDirection = this.props.horizontal ? DIRECTION.HORIZONTAL : DIRECTION.VERTICAL;
-
- componentWillReceiveProps(nextProps) {
- const {
- estimatedItemSize,
- children,
- itemSize,
- scrollOffset,
- scrollToAlignment,
- scrollToIndex,
- horizontal,
- nestedList,
- active,
- } = this.props;
- const scrollPropsHaveChanged =
- nextProps.scrollToIndex !== scrollToIndex ||
- nextProps.scrollToAlignment !== scrollToAlignment;
- const nestedActiveChanged = nestedList && nextProps.active !== active;
- const itemPropsHaveChanged =
- nestedActiveChanged ||
- nextProps.children.length !== children.length ||
- nextProps.itemSize !== itemSize ||
- nextProps.estimatedItemSize !== estimatedItemSize;
-
- if (nextProps.itemSize !== itemSize) {
- this.sizeAndPositionManager.updateConfig({
- itemSizeGetter: this.itemSizeGetter(nextProps.itemSize),
- });
- }
-
- this.scrollDirection = horizontal ? DIRECTION.HORIZONTAL : DIRECTION.VERTICAL;
- if (
- nestedActiveChanged ||
- nextProps.children.length !== children.length ||
- nextProps.estimatedItemSize !== estimatedItemSize
- ) {
- this.sizeAndPositionManager.updateConfig({
- itemCount: nextProps.children.length,
- estimatedItemSize: this.getEstimatedItemSize(nextProps),
- });
- }
-
- if (itemPropsHaveChanged) {
- this.recomputeSizes();
- }
-
- if (nextProps.scrollOffset !== scrollOffset) {
- this.setState({
- offset: nextProps.scrollOffset || 0,
- scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED,
- });
- } else if (
- typeof nextProps.scrollToIndex === 'number' &&
- (scrollPropsHaveChanged || itemPropsHaveChanged)
- ) {
- this.setState({
- offset: this.getOffsetForIndex(
- nextProps.scrollToIndex,
- nextProps.scrollToAlignment,
- nextProps.children.length,
- ),
- scrollChangeReason: SCROLL_CHANGE_REASON.REQUESTED,
- });
- }
- }
-
- getOffsetForIndex(
- index,
- scrollToAlignment = this.props.scrollToAlignment,
- itemCount = this.props.children.length,
- ) {
- const {style = {}} = this.props;
-
- if (index < 0 || index >= itemCount) {
- index = 0;
- }
-
- return this.sizeAndPositionManager.getUpdatedOffsetForIndex({
- align: scrollToAlignment,
- containerSize: style[sizeProp[this.scrollDirection]],
- currentOffset: this.state && this.state.offset || 0,
- targetIndex: index,
- });
- }
-
- recomputeSizes(startIndex = 0) {
- this.styleCache = {};
- this.sizeAndPositionManager.resetItem(startIndex);
- }
-
- getClientWidth() {
- return document.documentElement.clientWidth;
- }
-
- getRenderProps(options) {
- const {
- style = {},
- offset,
- overscanCount,
- totalSize,
- stickyIndices,
- children,
- width,
- } = options;
- const items = [];
- const wrapperStyle = {...STYLE_WRAPPER, ...style, width};
-
- const { start, stop } = this.sizeAndPositionManager.getVisibleRange({
- containerSize: options[sizeProp[this.scrollDirection]] || style[sizeProp[this.scrollDirection]] || 0,
- offset,
- overscanCount,
- });
- const innerStyle = {
- ...STYLE_INNER,
- [sizeProp[this.scrollDirection]]: this.sizeAndPositionManager.getTotalSize(totalSize),
- };
- let renderChildren = [];
-
- if (stickyIndices != null && stickyIndices.length !== 0) {
- stickyIndices.forEach((index) => {
- const child = children[index];
- /**
- * child.props.style = newStyle cause: Uncaught TypeError: Cannot add property style, object is not extensible
- */
- items.push({
- ...child,
- props: {
- ...child.props,
- style: {
- ...child.props.style,
- ...this.getStyle(index, true)
- }
- }
- });
- });
-
- if (this.scrollDirection === DIRECTION.HORIZONTAL) {
- innerStyle.display = 'flex';
- }
- }
-
- /**
- * solve children is not an array.
- */
- if (typeof start !== 'undefined' && typeof stop !== 'undefined' && children.slice) {
- let index = start;
- renderChildren = children.slice(start, stop + 1);
- renderChildren.forEach((child) => {
- /**
- * child.props.style = newStyle cause: Uncaught TypeError: Cannot add property style, object is not extensible
- */
- items.push({
- ...child,
- props: {
- ...child.props,
- style: {
- ...child.props.style,
- ...this.getStyle(index, false)
- }
- }
- });
- index++;
- });
- }
-
- return {
- wrapperStyle,
- innerStyle,
- nodeItems: items,
- };
- }
-}
diff --git a/packages/rax-recyclerview/src/VirtualizedList/NestedList.js b/packages/rax-recyclerview/src/VirtualizedList/NestedList.js
deleted file mode 100644
index 6185edb9..00000000
--- a/packages/rax-recyclerview/src/VirtualizedList/NestedList.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { createElement } from 'rax';
-import View from 'rax-view';
-import BaseList from './BaseList';
-import {
- SCROLL_CHANGE_REASON,
-} from './constants';
-
-let cacheOffset = 0;
-export default class NestedList extends BaseList {
- state = {
- offset: 0,
- };
-
- componentDidMount() {
- window.addEventListener('recyclerViewScroll', this.handleScroll);
- }
-
- componentWillUnMount() {
- window.removeEventListener('recyclerViewScroll', this.handleScroll);
- }
-
- handleScroll = (event) => {
- const { detail } = event;
- const { offset } = detail;
- const { active } = this.props;
-
- cacheOffset = offset;
- if (active) {
- this.setState({
- offset,
- scrollChangeReason: SCROLL_CHANGE_REASON.OBSERVED,
- });
- }
- }
-
- render() {
- const {
- className,
- } = this.props;
-
- const { innerStyle, nodeItems, wrapperStyle } = this.getRenderProps({
- ...this.props,
- offset: cacheOffset,
- });
-
- return (
-
- {nodeItems}
-
- );
- }
-}
diff --git a/packages/rax-recyclerview/src/VirtualizedList/NoRecycleList.tsx b/packages/rax-recyclerview/src/VirtualizedList/NoRecycleList.tsx
new file mode 100644
index 00000000..4dbd9cbe
--- /dev/null
+++ b/packages/rax-recyclerview/src/VirtualizedList/NoRecycleList.tsx
@@ -0,0 +1,24 @@
+/**
+ * backward compatibility: itemSize is optional
+ */
+
+import { createElement, forwardRef } from 'rax';
+import ScrollView from 'rax-scrollview';
+
+import { VirtualizedList } from './types';
+
+const NoRecycleList: VirtualizedList = forwardRef((props, ref) => {
+ const { children, ...rest } = props;
+
+ return (
+
+ {children}
+
+ );
+});
+
+export default NoRecycleList;
\ No newline at end of file
diff --git a/packages/rax-recyclerview/src/VirtualizedList/SizeAndPositionManager.js b/packages/rax-recyclerview/src/VirtualizedList/SizeAndPositionManager.js
deleted file mode 100644
index 9dfb66d7..00000000
--- a/packages/rax-recyclerview/src/VirtualizedList/SizeAndPositionManager.js
+++ /dev/null
@@ -1,277 +0,0 @@
-import {ALIGNMENT} from './constants';
-
-export default class SizeAndPositionManager {
- itemSizeGetter;
- itemCount;
- estimatedItemSize;
- lastMeasuredIndex;
- itemSizeAndPositionData;
-
- constructor({itemCount, itemSizeGetter, estimatedItemSize}) {
- this.itemSizeGetter = itemSizeGetter;
- this.itemCount = itemCount;
- this.estimatedItemSize = estimatedItemSize;
-
- // Cache of size and position data for items, mapped by item index.
- this.itemSizeAndPositionData = {};
-
- // Measurements for items up to this index can be trusted; items afterward should be estimated.
- this.lastMeasuredIndex = -1;
- }
-
- updateConfig({
- itemCount,
- itemSizeGetter,
- estimatedItemSize,
- }) {
- if (itemCount != null) {
- this.itemCount = itemCount;
- }
-
- if (estimatedItemSize != null) {
- this.estimatedItemSize = estimatedItemSize;
- }
-
- if (itemSizeGetter != null) {
- this.itemSizeGetter = itemSizeGetter;
- }
- }
-
- getLastMeasuredIndex() {
- return this.lastMeasuredIndex;
- }
-
- /**
- * This method returns the size and position for the item at the specified index.
- * It just-in-time calculates (or used cached values) for items leading up to the index.
- */
- getSizeAndPositionForIndex(index) {
- if (index < 0 || index >= this.itemCount) {
- throw Error(
- `Requested index ${index} is outside of range 0..${this.itemCount}`,
- );
- }
-
- if (index > this.lastMeasuredIndex) {
- const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
- let offset =
- lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size;
-
- for (let i = this.lastMeasuredIndex + 1; i <= index; i++) {
- const size = this.itemSizeGetter(i);
-
- if (size == null || isNaN(size)) {
- throw Error(`Invalid size returned for index ${i} of value ${size}`);
- }
-
- this.itemSizeAndPositionData[i] = {
- offset,
- size,
- };
-
- offset += size;
- }
-
- this.lastMeasuredIndex = index;
- }
-
- return this.itemSizeAndPositionData[index];
- }
-
- getSizeAndPositionOfLastMeasuredItem() {
- return this.lastMeasuredIndex >= 0
- ? this.itemSizeAndPositionData[this.lastMeasuredIndex]
- : {offset: 0, size: 0};
- }
-
- /**
- * Total size of all items being measured.
- * This value will be completedly estimated initially.
- * As items as measured the estimate will be updated.
- */
- getTotalSize(totalSize) {
- if (totalSize) {
- return totalSize;
- } else {
- const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
-
- return (
- lastMeasuredSizeAndPosition.offset +
- lastMeasuredSizeAndPosition.size +
- (this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize
- );
- }
- }
-
- /**
- * Determines a new offset that ensures a certain item is visible, given the alignment.
- *
- * @param align Desired alignment within container; one of "start" (default), "center", or "end"
- * @param containerSize Size (width or height) of the container viewport
- * @return Offset to use to ensure the specified item is visible
- */
- getUpdatedOffsetForIndex({
- align = ALIGNMENT.START,
- containerSize,
- currentOffset,
- targetIndex,
- }) {
- if (containerSize <= 0) {
- return 0;
- }
-
- const datum = this.getSizeAndPositionForIndex(targetIndex);
- const maxOffset = datum.offset;
- const minOffset = maxOffset - containerSize + datum.size;
-
- let idealOffset;
-
- switch (align) {
- case ALIGNMENT.END:
- idealOffset = minOffset;
- break;
- case ALIGNMENT.CENTER:
- idealOffset = maxOffset - (containerSize - datum.size) / 2;
- break;
- case ALIGNMENT.START:
- idealOffset = maxOffset;
- break;
- default:
- idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset));
- }
-
- const totalSize = this.getTotalSize();
-
- return Math.max(0, Math.min(totalSize - containerSize, idealOffset));
- }
-
- getVisibleRange({
- containerSize,
- offset,
- overscanCount,
- }) {
- const totalSize = this.getTotalSize();
-
- if (totalSize === 0) {
- return {};
- }
-
- const maxOffset = offset + containerSize;
- let start = this.findNearestItem(offset);
-
- if (typeof start === 'undefined') {
- throw Error(`Invalid offset ${offset} specified`);
- }
-
- const datum = this.getSizeAndPositionForIndex(start);
- offset = datum.offset + datum.size;
-
- let stop = start;
-
- while (offset < maxOffset && stop < this.itemCount - 1) {
- stop++;
- offset += this.getSizeAndPositionForIndex(stop).size;
- }
-
- if (overscanCount) {
- start = Math.max(0, start - overscanCount);
- stop = Math.min(stop + overscanCount, this.itemCount - 1);
- }
-
- return {
- start,
- stop,
- };
- }
-
- /**
- * Clear all cached values for items after the specified index.
- * This method should be called for any item that has changed its size.
- * It will not immediately perform any calculations; they'll be performed the next time getSizeAndPositionForIndex() is called.
- */
- resetItem(index) {
- this.lastMeasuredIndex = Math.min(this.lastMeasuredIndex, index - 1);
- }
-
- /**
- * Searches for the item (index) nearest the specified offset.
- *
- * If no exact match is found the next lowest item index will be returned.
- * This allows partially visible items (with offsets just before/above the fold) to be visible.
- */
- findNearestItem(offset) {
- if (isNaN(offset)) {
- throw Error(`Invalid offset ${offset} specified`);
- }
-
- // Our search algorithms find the nearest match at or below the specified offset.
- // So make sure the offset is at least 0 or no match will be found.
- offset = Math.max(0, offset);
-
- const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
- const lastMeasuredIndex = Math.max(0, this.lastMeasuredIndex);
-
- if (lastMeasuredSizeAndPosition.offset >= offset) {
- // If we've already measured items within this range just use a binary search as it's faster.
- return this.binarySearch({
- high: lastMeasuredIndex,
- low: 0,
- offset,
- });
- } else {
- // If we haven't yet measured this high, fallback to an exponential search with an inner binary search.
- // The exponential search avoids pre-computing sizes for the full set of items as a binary search would.
- // The overall complexity for this approach is O(log n).
- return this.exponentialSearch({
- index: lastMeasuredIndex,
- offset,
- });
- }
- }
-
- binarySearch({
- low,
- high,
- offset,
- }) {
- let middle = 0;
- let currentOffset = 0;
-
- while (low <= high) {
- middle = low + Math.floor((high - low) / 2);
- currentOffset = this.getSizeAndPositionForIndex(middle).offset;
-
- if (currentOffset === offset) {
- return middle;
- } else if (currentOffset < offset) {
- low = middle + 1;
- } else if (currentOffset > offset) {
- high = middle - 1;
- }
- }
-
- if (low > 0) {
- return low - 1;
- }
-
- return 0;
- }
-
- exponentialSearch({index, offset}) {
- let interval = 1;
-
- while (
- index < this.itemCount &&
- this.getSizeAndPositionForIndex(index).offset < offset
- ) {
- index += interval;
- interval *= 2;
- }
-
- return this.binarySearch({
- high: Math.min(index, this.itemCount - 1),
- low: Math.floor(index / 2),
- offset,
- });
- }
-}
diff --git a/packages/rax-recyclerview/src/VirtualizedList/SizeAndPositionManager.ts b/packages/rax-recyclerview/src/VirtualizedList/SizeAndPositionManager.ts
new file mode 100644
index 00000000..46763946
--- /dev/null
+++ b/packages/rax-recyclerview/src/VirtualizedList/SizeAndPositionManager.ts
@@ -0,0 +1,183 @@
+import { TItemSize } from './types';
+
+type TSizeGetter = (i: number) => number;
+interface RenderInfo {
+ renderedIndexs: [number, number];
+ placeholderSizes: [number, number];
+ pageIndexs: [number, number];
+}
+class SizeAndPositionManager {
+ private bufferSize: number;
+ private readonly length: number;
+ private getSize: TSizeGetter;
+ private pages: {
+ startIndex: number;
+ endIndex: number;
+ startPos: number;
+ endPos: number;
+ }[] = [];
+
+ public totalSize: number;
+
+ public static bufferRatio = 1;
+ public static clientSize;
+ public static pixelRatio: number;
+
+ private isExactly: boolean = true;
+ private isCorrecting: boolean = false;
+
+ public constructor({ itemSize, horizontal, length, bufferSize, itemEstimateSize }: {
+ itemSize: TItemSize;
+ horizontal: boolean;
+ length: number;
+ bufferSize?: number;
+ itemEstimateSize?: number;
+ }) {
+ this.length = length;
+ this.bufferSize = this.getBufferSize(bufferSize, horizontal);
+ this.isExactly = !!itemSize;
+ this.getSize = this.initSizeGetter(itemSize || itemEstimateSize);
+ this.initManager();
+ }
+
+ private initManager() {
+ this.pages = [];
+ // sort items in pages
+ let size = 0;
+ let prevExtraSize = 0;
+ for (let i = 0; i < this.length;) {
+ const { endIndex, pageSize, extraSize } = this.calPageInfo(i, prevExtraSize);
+ this.pages.push({
+ startIndex: i,
+ endIndex,
+ startPos: size,
+ endPos: size + pageSize
+ });
+ i = endIndex + 1;
+ size += pageSize;
+ prevExtraSize = extraSize;
+ }
+ this.totalSize = size;
+ }
+
+ public correctSize(options: {targetNode: HTMLElement; headerLength: number; pageIndexs: [number, number]; styleString: string}) {
+ const {
+ targetNode,
+ headerLength,
+ pageIndexs: [prePageIndex, nextPageIndex],
+ styleString
+ } = options;
+ if (this.isExactly || this.isCorrecting) {
+ return;
+ }
+ this.isCorrecting = true;
+ const cellRoot = targetNode.parentNode;
+ const heights = [];
+ cellRoot.childNodes.forEach((child: HTMLElement) => heights.push(child.getBoundingClientRect()[styleString]));
+ let errorSize = 0;
+ let firstIndex = this.pages[prePageIndex].startIndex - headerLength - 1;
+ for (let i = prePageIndex; i <= nextPageIndex; i++) {
+ const page = this.pages[i];
+ const height = heights.slice(page.startIndex - firstIndex, page.endIndex + 1 - firstIndex).reduce((prev, cur) => {
+ return prev + cur;
+ }, 0) / SizeAndPositionManager.pixelRatio;
+ const pageErrorSize = height - page.endPos + page.startPos;
+ page.startPos = errorSize + page.startPos;
+ errorSize += pageErrorSize;
+ page.endPos = errorSize + page.endPos;
+ }
+ for (let i = nextPageIndex + 1; i < this.pages.length; i ++) {
+ this.pages[i].startPos += errorSize;
+ this.pages[i].endPos += errorSize;
+ };
+ this.totalSize += errorSize;
+ this.isCorrecting = false;
+ }
+
+ private calPageInfo(index: number, prevExtraSize: number) {
+ let size = 0;
+ while (index < this.length && size < this.bufferSize - prevExtraSize) {
+ size += this.getSize(index);
+ index++;
+ }
+ return {
+ endIndex: index - 1,
+ pageSize: size,
+ extraSize: size - this.bufferSize + prevExtraSize
+ };
+ }
+
+ private getBufferSize(bufferSize, horizontal): number {
+ if (bufferSize) {
+ return bufferSize;
+ }
+ if (horizontal) {
+ return SizeAndPositionManager.clientSize.width / SizeAndPositionManager.pixelRatio;
+ }
+ return SizeAndPositionManager.clientSize.height / SizeAndPositionManager.pixelRatio;
+ }
+
+ private initSizeGetter(itemSize) {
+ if (typeof itemSize === 'number') {
+ return () => {
+ return itemSize;
+ };
+ }
+ return (i) => {
+ const size = itemSize(i);
+ if (typeof size !== 'number') {
+ console.error(`rax-recyclerview: ${i} 节点的 size 为 ${size},不是 number 类型`);
+ return 0;
+ }
+ return size;
+ };
+ }
+
+ public calCurrent(scrollDistance: number): RenderInfo|void {
+ if (this.isCorrecting) {
+ return;
+ }
+ const distance = scrollDistance / SizeAndPositionManager.pixelRatio;
+ const bufferFont = distance - this.bufferSize;
+ const bufferEnd = distance + this.bufferSize * 2;
+
+ // TODO 二分查找
+ let prevPageIndex: number, nextPageIndex: number;
+ for (let i = 0; i < this.pages.length; i ++) {
+ if (this.pages[i].endPos > bufferFont) {
+ prevPageIndex = i;
+ break;
+ }
+ }
+ if (prevPageIndex === undefined) {
+ prevPageIndex = 0;
+ }
+ for (let i = prevPageIndex; i < this.pages.length; i++) {
+ if (this.pages[i].endPos >= bufferEnd) {
+ nextPageIndex = i;
+ break;
+ }
+ }
+ if (nextPageIndex === undefined) {
+ nextPageIndex = this.pages.length - 1;
+ }
+
+ const prevPage = this.pages[prevPageIndex];
+ const nextPage = this.pages[nextPageIndex];
+
+ if (prevPage && nextPage) {
+ return {
+ pageIndexs: [prevPageIndex, nextPageIndex],
+ placeholderSizes: [prevPage.startPos, this.totalSize - nextPage.endPos],
+ renderedIndexs: [prevPage.startIndex, nextPage.endIndex]
+ };
+ }
+ return {
+ pageIndexs: [0, 0],
+ placeholderSizes: [0, 0],
+ renderedIndexs: [0, this.length - 1]
+ };
+ }
+}
+
+export default SizeAndPositionManager;
\ No newline at end of file
diff --git a/packages/rax-recyclerview/src/VirtualizedList/constants.js b/packages/rax-recyclerview/src/VirtualizedList/constants.js
deleted file mode 100644
index 83d4411f..00000000
--- a/packages/rax-recyclerview/src/VirtualizedList/constants.js
+++ /dev/null
@@ -1,41 +0,0 @@
-export const ALIGNMENT = {
- AUTO: 'auto',
- START: 'start',
- CENTER: 'center',
- END: 'end',
-};
-
-export const DIRECTION = {
- HORIZONTAL: 'horizontal',
- VERTICAL: 'vertical',
-};
-
-export const SCROLL_CHANGE_REASON = {
- OBSERVED: 'observed',
- REQUESTED: 'requested',
-};
-
-export const scrollProp = {
- [DIRECTION.VERTICAL]: 'scrollTop',
- [DIRECTION.HORIZONTAL]: 'scrollLeft',
-};
-
-export const sizeProp = {
- [DIRECTION.VERTICAL]: 'height',
- [DIRECTION.HORIZONTAL]: 'width',
-};
-
-export const positionProp = {
- [DIRECTION.VERTICAL]: 'top',
- [DIRECTION.HORIZONTAL]: 'left',
-};
-
-export const marginProp = {
- [DIRECTION.VERTICAL]: 'marginTop',
- [DIRECTION.HORIZONTAL]: 'marginLeft',
-};
-
-export const oppositeMarginProp = {
- [DIRECTION.VERTICAL]: 'marginBottom',
- [DIRECTION.HORIZONTAL]: 'marginRight',
-};
diff --git a/packages/rax-recyclerview/src/VirtualizedList/index.js b/packages/rax-recyclerview/src/VirtualizedList/index.js
deleted file mode 100644
index 7c6e0a0a..00000000
--- a/packages/rax-recyclerview/src/VirtualizedList/index.js
+++ /dev/null
@@ -1,209 +0,0 @@
-import { PureComponent, createElement } from 'rax';
-import PropTypes from 'prop-types';
-import BaseList from './BaseList';
-import {
- SCROLL_CHANGE_REASON,
- scrollProp,
-} from './constants';
-import Timer from './timer';
-import NestedList from './NestedList';
-import throttle from './throttle';
-
-export {DIRECTION as ScrollDirection} from './constants';
-
-const STYLE_NODE_ID = 'rax-virtualized-list-style';
-const DEFAULT_SCROLL_CALLBACK_THROTTLE = 50;
-const DEFAULT_END_REACHED_THRESHOLD = 500;
-
-const STYLE_WRAPPER = {
- overflow: 'auto',
- willChange: 'transform',
- WebkitOverflowScrolling: 'touch',
-};
-
-export default class VirtualizedList extends BaseList {
- lastScrollDistance = 0;
- lastScrollContentSize = 0;
- loadmoreretry = 1;
-
- static defaultProps = {
- width: '100%',
- scrollEventThrottle: DEFAULT_SCROLL_CALLBACK_THROTTLE,
- onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
- showsHorizontalScrollIndicator: true,
- showsVerticalScrollIndicator: true,
- className: 'rax-virtualized-list',
- nestedList: false,
- };
-
- static propTypes = {
- onScroll: PropTypes.func,
- style: PropTypes.object,
- width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- onEndReached: PropTypes.func,
- onEndReachedThreshold: PropTypes.number,
- scrollEventThrottle: PropTypes.number,
- className: PropTypes.string,
- nestedList: PropTypes.bool,
- };
-
- rootNode;
-
- componentDidMount() {
- const { scrollOffset, scrollToIndex, scrollEventThrottle } = this.props;
- let handleScroll = this.handleScroll;
-
- if (scrollEventThrottle) {
- handleScroll = throttle(handleScroll, scrollEventThrottle);
- }
- this.rootNode.addEventListener('scroll', handleScroll, {
- passive: true,
- });
-
- if (scrollOffset != null) {
- this.scrollTo(scrollOffset);
- } else if (scrollToIndex != null) {
- this.scrollTo(this.getOffsetForIndex(scrollToIndex));
- }
- }
-
- componentDidUpdate(_, prevState) {
- const {offset, scrollChangeReason} = this.state;
-
- if (
- prevState.offset !== offset &&
- scrollChangeReason === SCROLL_CHANGE_REASON.REQUESTED
- ) {
- this.scrollTo(offset);
- }
- }
-
- componentWillUnmount() {
- this.rootNode.removeEventListener('scroll', this.handleScroll);
- }
-
- scrollTo(value, animated) {
- if (animated) {
- const timer = new Timer({
- duration: 400,
- easing: 'easeOutSine',
- onRun: (e) => {
- if (value >= 0) {
- const currentValue = this.rootNode[scrollProp[this.scrollDirection]];
- this.rootNode[scrollProp[this.scrollDirection]] = currentValue + e.percent * (value / this.pixelRatio - currentValue);
- }
- }
- });
- timer.run();
- } else {
- this.rootNode[scrollProp[this.scrollDirection]] = value / this.pixelRatio;
- }
- }
-
- render() {
- const {
- children,
- style = {},
- width,
- horizontal,
- showsHorizontalScrollIndicator,
- showsVerticalScrollIndicator,
- className,
- nestedList,
- ...props
- } = this.props;
- const { offset } = this.state;
- const wrapperStyle = {...STYLE_WRAPPER, ...style, width};
-
- let showsScrollIndicator = horizontal ? showsHorizontalScrollIndicator : showsVerticalScrollIndicator;
-
- if (!showsScrollIndicator && typeof document !== 'undefined' && !document.getElementById(STYLE_NODE_ID)) {
- let styleNode = document.createElement('style');
- styleNode.id = STYLE_NODE_ID;
- document.head.appendChild(styleNode);
- styleNode.innerHTML = `.${this.props.className}::-webkit-scrollbar{display: none;}`;
- }
-
- // return children when has nested list
- if (nestedList) {
- return (
-
- {children}
-
- );
- }
- const { innerStyle, nodeItems } = this.getRenderProps({
- ...this.props,
- offset,
- });
-
- return (
-
- );
- }
-
- getRef = (node) => {
- this.rootNode = node;
- };
-
- resetScroll = () => {
- this.lastScrollContentSize = 0;
- this.lastScrollDistance = 0;
- }
-
- handleScroll = (event) => {
- const { onScroll, onEndReached, onEndReachedThreshold, totalSize, horizontal: isHorizontal, nestedList } = this.props;
- const offset = this.getNodeOffset();
- const realOffset = offset * this.pixelRatio;
-
- if (
- offset < 0 ||
- this.state.offset === realOffset ||
- event.target !== this.rootNode
- ) {
- return;
- }
-
- if (!nestedList) {
- this.setState({
- offset: realOffset,
- scrollChangeReason: SCROLL_CHANGE_REASON.OBSERVED,
- });
- }
-
- if (typeof onScroll === 'function') {
- onScroll(event, offset);
- }
-
- if (typeof onEndReached === 'function') {
- const { scrollLeft, scrollTop, scrollHeight, scrollWidth, offsetWidth, offsetHeight } = this.rootNode;
- const scrollerNodeSize = isHorizontal ? offsetWidth : offsetHeight;
- const scrollContentSize = totalSize / this.pixelRatio || (isHorizontal ? scrollWidth : scrollHeight);
- const scrollDistance = isHorizontal ? scrollLeft : scrollTop;
- const isEndReached = scrollContentSize - scrollDistance - scrollerNodeSize < onEndReachedThreshold;
-
- const isScrollToEnd = scrollDistance > this.lastScrollDistance;
- const isLoadedMoreContent = scrollContentSize != this.lastScrollContentSize;
-
- if (isEndReached && isScrollToEnd && isLoadedMoreContent) {
- this.lastScrollContentSize = scrollContentSize;
- this.props.onEndReached();
- }
- this.lastScrollDistance = scrollDistance;
- window.dispatchEvent(new CustomEvent('recyclerViewEndReached'));
- }
- window.dispatchEvent(new CustomEvent('recyclerViewScroll', {
- detail: {
- offset: realOffset
- }
- }));
- };
-
- getNodeOffset() {
- return this.rootNode[scrollProp[this.scrollDirection]];
- }
-}
-
-VirtualizedList.NestedList = NestedList;
diff --git a/packages/rax-recyclerview/src/VirtualizedList/index.tsx b/packages/rax-recyclerview/src/VirtualizedList/index.tsx
new file mode 100644
index 00000000..b762727f
--- /dev/null
+++ b/packages/rax-recyclerview/src/VirtualizedList/index.tsx
@@ -0,0 +1,203 @@
+import { createElement, forwardRef, useState, useMemo, memo, Fragment, useRef, useLayoutEffect } from 'rax';
+import ScrollView from 'rax-scrollview';
+import View from 'rax-view';
+import Children from 'rax-children';
+import findDOMNode from 'rax-find-dom-node';
+
+import NoRecycleList from './NoRecycleList';
+import throttle from './throttle';
+
+import { VirtualizedList } from './types';
+import SizeAndPositionManager from './SizeAndPositionManager';
+import { isWeChatMiniProgram, isWeb } from '@uni/env';
+
+function createArray(length) {
+ if (length > 0) {
+ return new Array(length).fill(1);
+ }
+ return [];
+}
+
+function getConstantKey(horizontal: boolean) {
+ if (horizontal) {
+ return {
+ contentOffset: 'x',
+ style: 'width'
+ };
+ }
+ return {
+ contentOffset: 'y',
+ style: 'height'
+ };
+}
+
+const Cell = memo(({children}) => {
+ return (<>{children}>);
+});
+Cell.displayName = 'Cell';
+
+const Header = memo(({children, ...rest}) => {
+ return ({children});
+});
+Header.displayName = 'Header';
+
+const NestedList = memo(
+ forwardRef((props, ref) => {
+ return (
+
+ );
+ })
+);
+NestedList.displayName = 'NestedList';
+
+function getVirtualizedList(SizeAndPositionManager): VirtualizedList {
+ const VirtualizedList: VirtualizedList = forwardRef((props, ref) => {
+ const { itemSize, itemEstimateSize, horizontal, children, bufferSize, scrollEventThrottle = 50, ...rest } = props;
+ if (!itemSize && (!isWeb || !itemEstimateSize)) {
+ return ({children});
+ }
+ const {
+ headers,
+ cells,
+ cellLength
+ } = useMemo(() => {
+ const flattenChildren = Children.toArray(children);
+
+ // header and cell must list in order
+ let headerIndex = 0;
+ for (let i = 0; i < flattenChildren.length; i ++) {
+ if (flattenChildren[i].type !== Header) {
+ break;
+ }
+ headerIndex++;
+ }
+
+ const headers = flattenChildren.slice(0, headerIndex);
+ const cells = flattenChildren.slice(headerIndex);
+ return {
+ headers,
+ cells,
+ cellLength: cells.length
+ };
+ }, [children]);
+
+ const offsetRef = useRef(0);
+ const preNodeRef = useRef(null);
+
+ const constantKey = getConstantKey(horizontal);
+ const [renderInfo, setRenderInfo] = useState({
+ placeholderSizes: [0, 0],
+ renderedIndexs: [0, 0],
+ pageIndexs: [0, 0]
+ });
+
+ const manager: SizeAndPositionManager = useMemo(() => {
+ const manager: SizeAndPositionManager = new SizeAndPositionManager({
+ itemSize,
+ horizontal,
+ bufferSize,
+ itemEstimateSize,
+ length: cellLength,
+ });
+ const result = manager.calCurrent(offsetRef.current);
+ if (result) {
+ const {
+ renderedIndexs,
+ placeholderSizes,
+ pageIndexs
+ } = result;
+ setRenderInfo({
+ renderedIndexs,
+ placeholderSizes,
+ pageIndexs
+ });
+ }
+
+ return manager;
+ }, [itemSize, horizontal, cellLength, bufferSize, itemEstimateSize]);
+
+ function handleScroll(e) {
+ const offset = e.nativeEvent.contentOffset[constantKey.contentOffset];
+ offsetRef.current = offset;
+ const result = manager.calCurrent(offsetRef.current);
+ if (result) {
+ const {
+ renderedIndexs,
+ placeholderSizes,
+ pageIndexs
+ } = result;
+ setRenderInfo({
+ renderedIndexs,
+ placeholderSizes,
+ pageIndexs
+ });
+ }
+ props.onScroll && props.onScroll(e);
+ }
+
+ const scrollRef = useRef(handleScroll);
+ useMemo(() => {
+ scrollRef.current = handleScroll;
+ }, [manager, props.onScroll]);
+
+ const throttleScroll = useMemo(() => throttle((e) => scrollRef.current(e), scrollEventThrottle), [scrollEventThrottle]);
+
+ useLayoutEffect(() => {
+ if (isWeb) {
+ manager.correctSize({
+ targetNode: findDOMNode(preNodeRef.current),
+ headerLength: headers.length,
+ pageIndexs: renderInfo.pageIndexs as [number, number],
+ styleString: constantKey.style
+ });
+ }
+ }, [renderInfo.pageIndexs[0], renderInfo.pageIndexs[1]]);
+
+ if (isWeChatMiniProgram) {
+ return (
+
+ {/* fix sticky by adding view */}
+
+ {headers}
+
+ {createArray(renderInfo.renderedIndexs[0]).map((v, index) => )}
+ {cells.slice(renderInfo.renderedIndexs[0], renderInfo.renderedIndexs[1] + 1).map((child, index) => {child})}
+ {createArray(cellLength - renderInfo.renderedIndexs[1] - 1).map((v, index) => )}
+
+
+
+ );
+ }
+ return (
+
+ {headers}
+
+ {cells.slice(renderInfo.renderedIndexs[0], renderInfo.renderedIndexs[1] + 1)}
+
+
+ );
+ });
+
+ VirtualizedList.Header = Header;
+ VirtualizedList.Cell = Cell;
+ VirtualizedList.NestedList = NestedList;
+ VirtualizedList.displayName = 'RecyclerView';
+
+ return VirtualizedList;
+}
+
+export default getVirtualizedList;
\ No newline at end of file
diff --git a/packages/rax-recyclerview/src/VirtualizedList/throttle.js b/packages/rax-recyclerview/src/VirtualizedList/throttle.js
deleted file mode 100644
index ed62d7ca..00000000
--- a/packages/rax-recyclerview/src/VirtualizedList/throttle.js
+++ /dev/null
@@ -1,22 +0,0 @@
-export default function throttle(func, wait) {
- var ctx, args, rtn, timeoutID;
- var last = 0;
-
- function call() {
- timeoutID = 0;
- last = +new Date();
- rtn = func.apply(ctx, args);
- ctx = null;
- args = null;
- }
-
- return function throttled() {
- ctx = this;
- args = arguments;
- var delta = new Date() - last;
- if (!timeoutID)
- if (delta >= wait) call();
- else timeoutID = setTimeout(call, wait - delta);
- return rtn;
- };
-}
diff --git a/packages/rax-recyclerview/src/VirtualizedList/throttle.ts b/packages/rax-recyclerview/src/VirtualizedList/throttle.ts
new file mode 100644
index 00000000..30f57307
--- /dev/null
+++ b/packages/rax-recyclerview/src/VirtualizedList/throttle.ts
@@ -0,0 +1,27 @@
+export default function throttle(func: (...args: any[]) => void, wait: number) {
+ let ctx: any;
+ let args: any;
+ let rtn: any;
+ let timeoutID: number | ReturnType;
+ let last = 0;
+
+ function call() {
+ timeoutID = 0;
+ last = +new Date();
+ rtn = func.apply(ctx, args);
+ ctx = null;
+ args = null;
+ }
+
+ return function throttled() {
+ ctx = this;
+ args = arguments;
+ var delta = new Date().getTime() - last;
+ if (!timeoutID)
+ if (delta >= wait) call();
+ else {
+ timeoutID = setTimeout(call, wait - delta);
+ }
+ return rtn;
+ };
+}
diff --git a/packages/rax-recyclerview/src/VirtualizedList/timer.js b/packages/rax-recyclerview/src/VirtualizedList/timer.js
deleted file mode 100644
index cb693427..00000000
--- a/packages/rax-recyclerview/src/VirtualizedList/timer.js
+++ /dev/null
@@ -1,119 +0,0 @@
-const requestAnimationF = typeof requestAnimationFrame === 'undefined' ?
- typeof webkitRequestAnimationFrame === 'undefined' ?
- job => setTimeout(job, 16) :
- webkitRequestAnimationFrame : requestAnimationFrame;
-
-const cancelAnimationF = typeof cancelAnimationFrame === 'undefined' ?
- typeof webkitCancelAnimationFrame === 'undefined' ?
- clearTimeout :
- webkitCancelAnimationFrame : cancelAnimationFrame;
-
-const TYPES = {
- START: 'start',
- END: 'end',
- RUN: 'run',
- STOP: 'stop'
-};
-
-const easing = {
- easeOutSine(x) {
- return Math.sin(x * Math.PI / 2);
- }
-};
-
-const MIN_DURATION = 1;
-
-const noop = () => {};
-
-class Timer {
- constructor(config) {
- this.config = {
- easing: 'linear',
- duration: Infinity,
- onStart: noop,
- onRun: noop,
- onStop: noop,
- onEnd: noop,
- ...config,
- };
- }
-
- run() {
- let {duration, onStart, onRun} = this.config;
- if (duration <= MIN_DURATION) {
- this.isfinished = true;
- onRun({percent: 1});
- this.stop();
- }
- if (this.isfinished) return;
- this._hasFinishedPercent = this._stop && this._stop.percent || 0;
- this._stop = null;
- this.start = Date.now();
- this.percent = 0;
- onStart({percent: 0, type: TYPES.START});
- // epsilon determines the precision of the solved values
- let epsilon = 1000 / 60 / duration / 4;
- this.easingFn = easing[this.config.easing];
- this._run();
- }
-
- _run() {
- let {onRun, onStop} = this.config;
- this._raf && cancelAnimationF(this._raf);
- this._raf = requestAnimationF(() => {
- this.now = Date.now();
- this.t = this.now - this.start;
- this.duration = this.now - this.start >= this.config.duration ? this.config.duration : this.now - this.start;
- this.progress = this.easingFn(this.duration / this.config.duration);
- this.percent = this.duration / this.config.duration + this._hasFinishedPercent;
- if (this.percent >= 1 || this._stop) {
- this.percent = this._stop && this._stop.percent ? this._stop.percent : 1;
- this.duration = this._stop && this._stop.duration ? this._stop.duration : this.duration;
-
- onRun({
- percent: this.progress,
- originPercent: this.percent,
- t: this.t,
- type: TYPES.RUN
- });
-
- onStop({
- percent: this.percent,
- t: this.t,
- type: TYPES.STOP
- });
-
- if (this.percent >= 1) {
- this.isfinished = true;
- this.stop();
- }
- return;
- }
-
- onRun({
- percent: this.progress,
- originPercent: this.percent,
- t: this.t,
- type: TYPES.RUN
- });
-
- this._run();
- });
- }
-
- stop() {
- let {onEnd} = this.config;
- this._stop = {
- percent: this.percent,
- now: this.now
- };
- onEnd({
- percent: 1,
- t: this.t,
- type: TYPES.END
- });
- cancelAnimationF(this._raf);
- }
-}
-
-export default Timer;
\ No newline at end of file
diff --git a/packages/rax-recyclerview/src/VirtualizedList/types.ts b/packages/rax-recyclerview/src/VirtualizedList/types.ts
new file mode 100644
index 00000000..5e940836
--- /dev/null
+++ b/packages/rax-recyclerview/src/VirtualizedList/types.ts
@@ -0,0 +1,22 @@
+import { ScrollViewProps } from 'rax-scrollview';
+import { RefAttributes, HTMLAttributes, ForwardRefExoticComponent } from 'rax';
+
+export type TItemSize = number | ((e: number) => number);
+
+export interface RecyclerViewRefObject extends ScrollViewProps {
+ itemSize?: TItemSize;
+ totalSize?: number;
+ bufferSize?: number;
+ horizontal: boolean;
+ bufferRatio?: number;
+ scrollEventThrottle?: number;
+ itemEstimateSize?: number;
+}
+
+export interface LegacyRefObject extends RefAttributes, HTMLAttributes {}
+
+export interface VirtualizedList extends ForwardRefExoticComponent {
+ Header?: Rax.MemoExoticComponent> | Rax.NamedExoticComponent