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` 的包装,其具有复用内部组件来提供性能的机制。 + +使用时,如果为垂直方向滚动时,**必须设置高度**,否则元素回收功能和滚动相关的功能将失效。 ![](https://gw.alicdn.com/tfs/TB1Cf_ZRVXXXXa8XVXXXXXXXXXX-255-265.gif) @@ -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` 为参与回收的子元素的数组下标位置 | browserminiApp wechatMiniprogrambytedanceMicroApp | +| itemEstimateSize | `number` | - | × | 单位为`rpx`, 当子元素不是固定高度时,可以传入该值作为元素的估计值,本属性只支持 Web | browser | +| horizontal | `boolean` | - | false | 设置为横向滚动 | browser weexminiApp wechatMiniprogrambytedanceMicroApp quickApp | +| onEndReachedThreshold | `string/number` | 500 | ✘ | 设置加载更多的偏移, 推荐使用 string 格式来指指定尺寸单位,如`100rpx` | browser weex miniAppbytedanceMicroApp | +| onEndReached | `function` | - | ✘ | 滚动区域还剩`onEndReachedThreshold`的长度时触发 | browser weex miniApp wechatMiniprogrambytedanceMicroApp | +| onScroll | `function` | - | ✘ | 滚动时触发的事件,返回当前滚动的水平垂直距离 | browser weex miniApp wechatMiniprogrambytedanceMicroApp | +| bufferSize | `number` | - | ✘ | 缓冲区单位尺寸,单位为 rpx,默认为当前视图的尺寸(水如水平方向滚动时,值为 750), recyclerview 会默认渲染 3 屏缓冲区尺寸。注意在小程序中,慎用该属性! | browserminiApp wechatMiniprogrambytedanceMicroApp | +| onTouchStart | `function` | - | false | touchStart 触发的事件,返回触摸点数据(touches、changedTouches) | browser weexminiApp | +| onTouchMove | `function` | - | false | touchMove 触发的事件,返回触摸点数据(touches、changedTouches) | browser weexminiApp | +| onTouchEnd | `function` | - | false | touchEnd 触发的事件,返回触摸点数据(touches、changedTouches) | browser weexminiApp | +| onTouchCancel | `function` | - | false | touchCancel 触发的事件,返回触摸点数据(touches、changedTouches) | miniApp | +| disableScroll | `boolean` | - | false | 是否禁止滚动,是否禁止滚动, rax-recyclerview@1.3.4 及以上版本支 | browser miniApp | + +## 子组件 + +### RecyclerView.Header + +头部子元素需要用 `RecycerView.Header` 包裹,头部元素**不参与**元素回收。 -| 属性 | 类型 | 默认值 | 必填 | 描述 | 支持 | -| --------------------- | ----------------- | ------ | ---- | ----------------------------------------------- | ------------------------------------------------------------ | -| onEndReachedThreshold | `number` | 500 | ✘ | 设置加载更多的偏移 | browser weex miniAppbytedanceMicroApp | -| onEndReached | `function` | - | ✘ | 滚动区域还剩`onEndReachedThreshold`的长度时触发 | browser weex miniApp wechatMiniprogrambytedanceMicroApp | -| onScroll | `function` | - | ✘ | 滚动时触发的事件,返回当前滚动的水平垂直距离 | browser weex miniApp wechatMiniprogrambytedanceMicroApp | -| itemSize | `function/number` | - | ✘ | 返回每个 cell 的高度(节点回收时需要) | browser | -| totalSize | `number` | - | ✘ | 当前列表总高度(在 cell 高度可变的列表中需要传) | browser | -| onTouchStart | `function` | - | false | touchStart触发的事件,返回触摸点数据(touches、changedTouches) | browser weexminiApp | -| onTouchMove | `function` | - | false | touchMove触发的事件,返回触摸点数据(touches、changedTouches) | browser weexminiApp | -| onTouchEnd | `function` | - | false | touchEnd触发的事件,返回触摸点数据(touches、changedTouches) | browser weexminiApp | -| onTouchCancel | `function` | - | false | touchCancel触发的事件,返回触摸点数据(touches、changedTouches) | miniApp | -| disableScroll | `boolean` | - | false | 是否禁止滚动,是否禁止滚动, rax-recyclerview@1.3.4 及以上版本支 | browser miniApp | +### 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 ( -
-
{nodeItems}
-
- ); - } - - 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; + Cell?: Rax.MemoExoticComponent> | Rax.NamedExoticComponent; + NestedList?: Rax.MemoExoticComponent>; +} \ No newline at end of file diff --git a/packages/rax-recyclerview/src/__tests__/RecyclerView.js b/packages/rax-recyclerview/src/__tests__/RecyclerView.js index bc79daf6..630aaf8a 100644 --- a/packages/rax-recyclerview/src/__tests__/RecyclerView.js +++ b/packages/rax-recyclerview/src/__tests__/RecyclerView.js @@ -1,6 +1,6 @@ import {createElement, Component} from 'rax'; import renderer from 'rax-test-renderer'; -import RecyclerView from '../'; +import RecyclerView from '../../lib'; class RecyclerViewTest extends Component { renderHeader() { diff --git a/packages/rax-recyclerview/src/__tests__/RecyclerView.weex.js b/packages/rax-recyclerview/src/__tests__/RecyclerView.weex.js index dd6fabd5..09dce1f9 100644 --- a/packages/rax-recyclerview/src/__tests__/RecyclerView.weex.js +++ b/packages/rax-recyclerview/src/__tests__/RecyclerView.weex.js @@ -1,6 +1,6 @@ import {createElement, Component} from 'rax'; import renderer from 'rax-test-renderer'; -import RecyclerView from '../'; +import RecyclerView from '../../lib'; jest.mock('universal-env', () => { return { diff --git a/packages/rax-recyclerview/src/index.js b/packages/rax-recyclerview/src/index.js deleted file mode 100644 index e0fde969..00000000 --- a/packages/rax-recyclerview/src/index.js +++ /dev/null @@ -1,176 +0,0 @@ -import { - createElement, - createContext, - useContext, - forwardRef, - memo, - useState, - useRef, - useImperativeHandle -} from 'rax'; -import { isWeex } from 'universal-env'; -import View from 'rax-view'; -import findDOMNode from 'rax-find-dom-node'; -import RefreshControl from 'rax-refreshcontrol'; -import ScrollView from 'rax-scrollview'; -import Children from 'rax-children'; -import VirtualizedList from './VirtualizedList/index'; - -const Context = createContext(true); - -const Cell = memo( - forwardRef(({ className, style, ...rest }, ref) => { - const isInARecyclerView = useContext(Context); - return isWeex && isInARecyclerView ? ( - - ) : ( - - ); - }) -); -Cell.displayName = 'Cell'; - -const Header = memo( - forwardRef(({ className, style, ...rest }, ref) => { - const isInARecyclerView = useContext(Context); - return isWeex && isInARecyclerView ? ( -
- ) : ( - - ); - }) -); -Header.displayName = 'Header'; - -const NestedList = memo( - forwardRef(({ className, style, ...rest }, ref) => { - const isInARecyclerView = useContext(Context); - return !isWeex && isInARecyclerView ? ( - - ) : ( - - ); - }) -); -NestedList.displayName = 'NestedList'; - -const RecyclerView = forwardRef((props, ref) => { - const { className, style, ...rest } = props; - const [loadmoreretry, setLoadmoreretry] = useState(0); - const scrollview = useRef(null); - const list = useRef(null); - const firstNodePlaceholder = useRef(null); - const needRecycler = props.itemSize || props.nestedList ? true : false; - - const handleScroll = e => { - e.nativeEvent = { - contentOffset: { - // HACK: weex scroll event value is opposite of web - x: -e.contentOffset.x, - y: -e.contentOffset.y - }, - contentSize: e.contentSize - ? { - width: e.contentSize.width, - height: e.contentSize.height - } - : null - }; - props.onScroll(e); - }; - - - useImperativeHandle(ref, () => ({ - _nativeNode: isWeex ? list.current : needRecycler ? scrollview.current : scrollview.current._nativeNode, - resetScroll() { - if (isWeex) { - setLoadmoreretry(loadmoreretry + 1); // for weex 0.9- - list.current.resetLoadmore && list.current.resetLoadmore(); // for weex 0.9+ - } else { - scrollview.current.resetScroll(); - } - }, - scrollTo(options) { - let x = parseInt(options.x); - let y = parseInt(options.y); - let animated = - options && typeof options.animated !== 'undefined' - ? options.animated - : true; - - if (isWeex) { - let dom = __weex_require__('@weex-module/dom'); - let firstNode = findDOMNode(firstNodePlaceholder.current); - dom.scrollToElement(firstNode, { - offset: x || y || 0, - animated - }); - } else if (needRecycler) { - scrollview.current.scrollTo(x || y, animated); - } else { - scrollview.current.scrollTo(options); - } - } - })); - - if (isWeex) { - let cells = Children.map(props.children, (child, index) => { - if (child) { - let hasOnRefresh = - child.props && typeof child.props.onRefresh == 'function'; - if ( - props._autoWrapCell && - child.type != RefreshControl && - child.type != Header && - !hasOnRefresh - ) { - return {child}; - } else { - return child; - } - } else { - return ; - } - }); - - // add firstNodePlaceholder after refreshcontrol - if (cells && cells.length) { - let addIndex = cells[0].type == Cell || cells[0].type == Header ? 0 : 1; - cells.splice(addIndex, 0, ); - } - - return ( - - - {cells} - - - ); - } else { - if (needRecycler) { - return ( - - ); - } else { - return ( - - ); - } - } -}); - -RecyclerView.Header = Header; -RecyclerView.Cell = Cell; -RecyclerView.NestedList = NestedList; -RecyclerView.displayName = 'RecyclerView'; - -export default RecyclerView; diff --git a/packages/rax-recyclerview/src/index.tsx b/packages/rax-recyclerview/src/index.tsx new file mode 100644 index 00000000..83f19d5e --- /dev/null +++ b/packages/rax-recyclerview/src/index.tsx @@ -0,0 +1,20 @@ +import { isWeb, isWeex, isMiniApp, isWeChatMiniProgram, isByteDanceMicroApp, isBaiduSmartProgram, isKuaiShouMiniProgram } from '@uni/env'; +import RecyclerViewWeb from './web'; +import RecyclerViewWeex from './weex'; +import RecyclerViewMiniProgram from './miniapp-runtime'; + +let RecyclerView = null; + +if (isWeex) { + RecyclerView = RecyclerViewWeex; +} else if (isWeb) { + RecyclerView = RecyclerViewWeb; +} else if (isMiniApp || isWeChatMiniProgram || isByteDanceMicroApp || isBaiduSmartProgram || isKuaiShouMiniProgram) { + RecyclerView = RecyclerViewMiniProgram; +} else { + RecyclerView = RecyclerViewWeb; +} + +export * from './VirtualizedList/types'; +export default RecyclerView; + diff --git a/packages/rax-recyclerview/src/miniapp-runtime/getInfoSync.ts b/packages/rax-recyclerview/src/miniapp-runtime/getInfoSync.ts new file mode 100644 index 00000000..31d8f312 --- /dev/null +++ b/packages/rax-recyclerview/src/miniapp-runtime/getInfoSync.ts @@ -0,0 +1,41 @@ +/* eslint-disable */ +import { isMiniApp, isWeChatMiniProgram, isByteDanceMicroApp, isBaiduSmartProgram, isKuaiShouMiniProgram } from 'universal-env'; + + +export default function getInfoSync(): { + windowHeight: number; + windowWidth: number; +} { + if (isWeChatMiniProgram) { + // @ts-ignore + return wx.getSystemInfoSync(); + } + if (isByteDanceMicroApp) { + // @ts-ignore + return tt.getSystemInfoSync(); + } + + if (isMiniApp) { + // @ts-ignore + const isDingdingMiniapp = typeof dd !== 'undefined' && dd !== null && typeof dd.alert !== 'undefined'; + if (isDingdingMiniapp) { + // @ts-ignore + return dd.getSystemInfoSync(); + } + return my.getSystemInfoSync(); + } + + if (isBaiduSmartProgram) { + // @ts-ignore + return swan.getSystemInfoSync(); + } + + if (isKuaiShouMiniProgram) { + // @ts-ignore + return ks.getSystemInfoSync(); + } + + throw new Error('getInfoSync 暂不支持'); +} + + diff --git a/packages/rax-recyclerview/src/miniapp-runtime/index.ts b/packages/rax-recyclerview/src/miniapp-runtime/index.ts new file mode 100644 index 00000000..7ea424fe --- /dev/null +++ b/packages/rax-recyclerview/src/miniapp-runtime/index.ts @@ -0,0 +1,14 @@ +import getInfoSync from './getInfoSync'; +import getVirtualizedList from '../VirtualizedList'; +import SizeAndPositionManager from '../VirtualizedList/SizeAndPositionManager'; + +const FULL_WIDTH = 750; +const { windowHeight, windowWidth } = getInfoSync(); +SizeAndPositionManager.clientSize = { + width: windowWidth, + height: windowHeight +}; +SizeAndPositionManager.pixelRatio = windowWidth / FULL_WIDTH; + +const VirtualizedList = getVirtualizedList(SizeAndPositionManager); +export default VirtualizedList; \ No newline at end of file diff --git a/packages/rax-recyclerview/src/web/index.tsx b/packages/rax-recyclerview/src/web/index.tsx new file mode 100644 index 00000000..a076b000 --- /dev/null +++ b/packages/rax-recyclerview/src/web/index.tsx @@ -0,0 +1,12 @@ +import SizeAndPositionManager from '../VirtualizedList/SizeAndPositionManager'; +import getVirtualizedList from '../VirtualizedList'; + +const FULL_WIDTH = 750; +SizeAndPositionManager.clientSize = { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight +}; +SizeAndPositionManager.pixelRatio = document.documentElement.clientWidth / FULL_WIDTH; + +const VirtualizedList = getVirtualizedList(SizeAndPositionManager); +export default VirtualizedList; \ No newline at end of file diff --git a/packages/rax-recyclerview/src/weex/index.js b/packages/rax-recyclerview/src/weex/index.js new file mode 100644 index 00000000..528d850b --- /dev/null +++ b/packages/rax-recyclerview/src/weex/index.js @@ -0,0 +1,163 @@ +import { + createElement, + createContext, + useContext, + forwardRef, + memo, + useState, + useRef, + useImperativeHandle +} from 'rax'; +import View from 'rax-view'; +import findDOMNode from 'rax-find-dom-node'; +import RefreshControl from 'rax-refreshcontrol'; +import getElementById from 'rax-get-element-by-id'; +import Children from 'rax-children'; + +const isWeexV2 = typeof __weex_v2__ === 'object'; + +const Context = createContext(true); + +const Cell = memo( + forwardRef(({ className, style, ...rest }, ref) => { + return ( + + ); + }) +); +Cell.displayName = 'Cell'; + +const Header = memo( + forwardRef(({ className, style, ...rest }, ref) => { + const isInARecyclerView = useContext(Context); + return ( +
+ ); + }) +); +Header.displayName = 'Header'; + +const NestedList = memo( + forwardRef(({ className, style, ...rest }, ref) => { + return ( + + ); + }) +); +NestedList.displayName = 'NestedList'; + +const RecyclerView = forwardRef((props, ref) => { + const { className, style, ...rest } = props; + const [loadmoreretry, setLoadmoreretry] = useState(0); + const list = useRef(null); + const firstNodePlaceholder = useRef(null); + + const handleScroll = e => { + e.nativeEvent = { + contentOffset: { + // HACK: weex scroll event value is opposite of web + x: -e.contentOffset.x, + y: -e.contentOffset.y + }, + contentSize: e.contentSize + ? { + width: e.contentSize.width, + height: e.contentSize.height + } + : null + }; + props.onScroll(e); + }; + + + useImperativeHandle(ref, () => ({ + _nativeNode: list.current, + resetScroll() { + setLoadmoreretry(loadmoreretry + 1); // for weex 0.9- + list.current.resetLoadmore && list.current.resetLoadmore(); // for weex 0.9+ + }, + scrollTo(options) { + let firstNode = findDOMNode(firstNodePlaceholder.current); + if (isWeexV2) { + list.current.scrollTo(firstNode, options); + } else { + let x = parseInt(options.x); + let y = parseInt(options.y); + let animated = + options && typeof options.animated !== 'undefined' + ? options.animated + : true; + + let dom = __weex_require__('@weex-module/dom'); + dom.scrollToElement(firstNode, { + offset: x || y || 0, + animated + }); + } + }, + scrollIntoView(options) { + const { id, animated = true } = options || {}; + if (!id) { + throw new Error('Params missing id.'); + } + const node = getElementById(id); + if (node) { + if (isWeexV2) { + list.current.scrollTo(node, options); + } else { + const dom = __weex_require__('@weex-module/dom'); + dom.scrollToElement(node, { + animated + }); + } + } + } + })); + + let cells = Children.map(props.children, (child, index) => { + if (child) { + let hasOnRefresh = + child.props && typeof child.props.onRefresh == 'function'; + if ( + props._autoWrapCell && + child.type != RefreshControl && + child.type != Header && + !hasOnRefresh + ) { + return {child}; + } else { + return child; + } + } else { + return ; + } + }); + + // add firstNodePlaceholder after refreshcontrol + if (cells && cells.length) { + let addIndex = cells[0].type == Cell || cells[0].type == Header ? 0 : 1; + cells.splice(addIndex, 0, ); + } + + return ( + + {cells} + + ); +}); + +RecyclerView.Header = Header; +RecyclerView.Cell = Cell; +RecyclerView.NestedList = NestedList; +RecyclerView.displayName = 'RecyclerView'; + +export default RecyclerView; diff --git a/packages/rax-scrollview/CHANGELOG.md b/packages/rax-scrollview/CHANGELOG.md index 0eef046b..292f0ed9 100644 --- a/packages/rax-scrollview/CHANGELOG.md +++ b/packages/rax-scrollview/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.7.1 + +- Replace `ref` with `forwardRef` in `useImperativeHandle` if `forwardRef` exists + ## 3.7.0 - Support weex v2 diff --git a/packages/rax-scrollview/package.json b/packages/rax-scrollview/package.json index f746f172..ffa03cdf 100644 --- a/packages/rax-scrollview/package.json +++ b/packages/rax-scrollview/package.json @@ -1,6 +1,6 @@ { "name": "rax-scrollview", - "version": "3.7.0", + "version": "3.7.1-1", "description": "ScrollView component for Rax.", "license": "BSD-3-Clause", "main": "lib/index.js", diff --git a/packages/rax-scrollview/src/miniapp/index.tsx b/packages/rax-scrollview/src/miniapp/index.tsx index 01ff0038..2422d85a 100644 --- a/packages/rax-scrollview/src/miniapp/index.tsx +++ b/packages/rax-scrollview/src/miniapp/index.tsx @@ -12,6 +12,7 @@ import { ScrollViewProps } from '../types'; import wrapDefaultProperties from '../utils/wrapDefaultProperties'; import { getInfoSync } from '@uni/system-info'; import '../index.css'; +import omit from '../utils/omit'; const FULL_WIDTH = 750; const ANIMATION_DURATION = 400; @@ -56,7 +57,8 @@ const ScrollView: ForwardRefExoticComponent = forwardRef( onScroll, children, disableScroll = false, - onEndReachedThreshold + onEndReachedThreshold, + forwardRef } = props; const [scrollTop] = useState(0); const [scrollLeft] = useState(0); @@ -83,7 +85,7 @@ const ScrollView: ForwardRefExoticComponent = forwardRef( onScroll(e); } }; - useImperativeHandle(ref, () => ({ + useImperativeHandle(forwardRef ? forwardRef : ref, () => ({ _nativeNode: scrollerRef.current, resetScroll() { if (horizontal) { @@ -142,7 +144,7 @@ const ScrollView: ForwardRefExoticComponent = forwardRef( return ( , fields: string[]): Record { + const shallowCopy = Object.assign({}, obj); + + for (let i = 0; i < fields.length; i++) { + const key = fields[i]; + if (shallowCopy.hasOwnProperty(key)) { + delete shallowCopy[key]; + } + } + + return shallowCopy; +} \ No newline at end of file diff --git a/packages/rax-scrollview/src/web/index.tsx b/packages/rax-scrollview/src/web/index.tsx index 3a966776..74bec582 100644 --- a/packages/rax-scrollview/src/web/index.tsx +++ b/packages/rax-scrollview/src/web/index.tsx @@ -102,7 +102,8 @@ const ScrollView: ForwardRefExoticComponent = forwardRef( onEndReached, onEndReachedThreshold, onScroll, - children + children, + forwardRef } = props; const lastScrollDistance = useRef(0); const lastScrollContentSize = useRef(0); @@ -159,7 +160,7 @@ const ScrollView: ForwardRefExoticComponent = forwardRef( lastScrollDistance.current = scrollDistance; } }; - useImperativeHandle(ref, () => ({ + useImperativeHandle(forwardRef ? forwardRef : ref, () => ({ _nativeNode: scrollerRef.current, resetScroll() { lastScrollContentSize.current = 0;