diff --git a/src/config.json b/src/config.json index 72f67895889530c1b9855433891c32cd2f89f1b7..9c3a8ce710d698fe86ea76ea3d9b244aacf7eb7f 100644 --- a/src/config.json +++ b/src/config.json @@ -1039,8 +1039,9 @@ "cName": "虚拟列表", "desc": "长列表渲染", "sort": 20, + "tarodoc":true, "show": true, - "taro": false, + "taro": true, "author": "hx" }, { diff --git a/src/packages/textarea/demo.taro.tsx b/src/packages/textarea/demo.taro.tsx index 16001fe5873a255c706e5890195630e851661994..6dd95189dc92a03ef3ada9457ed19e61ceae8d4f 100644 --- a/src/packages/textarea/demo.taro.tsx +++ b/src/packages/textarea/demo.taro.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react' +import Taro from '@tarojs/taro' import { useTranslate } from '@/sites/assets/locale/taro' import { TextArea } from '@/packages/nutui.react.taro' import Header from '@/sites/components/header' -import Taro from '@tarojs/taro' interface T { basic: string diff --git a/src/packages/virtuallist/demo.taro.tsx b/src/packages/virtuallist/demo.taro.tsx index 194d1012c0071bc1d4c8efc41041a68db4788ea4..6b9934e83fed8c0e2fa0786ed041beb25c908a39 100644 --- a/src/packages/virtuallist/demo.taro.tsx +++ b/src/packages/virtuallist/demo.taro.tsx @@ -1,9 +1,13 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { useTranslate } from '@/sites/assets/locale/taro' -import { Cell, CellGroup, Radio } from '@/packages/nutui.react.taro' +import { + Cell, + CellGroup, + Radio, + VirtualList, +} from '@/packages/nutui.react.taro' import Header from '@/sites/components/header' import Taro from '@tarojs/taro' -import VirtualList from './index' const { RadioGroup } = Radio @@ -32,6 +36,25 @@ const ListDemo = () => { const [pageNo, setPageNo] = useState(1) const [radioVal, setRadioVal] = useState('1') const [isLoading, setIsLoading] = useState(false) + + const itemStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '50px', + background: '#fff', + borderRadius: '10px', + } + const itemStyel2 = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + background: '#fff', + borderRadius: '10px', + } + const handleChange = (v: any) => { setRadioVal(v) setPageNo(1) @@ -53,16 +76,26 @@ const ListDemo = () => { }) } }, [pageNo]) + + useEffect(() => { + getData() + }, [getData]) + const ItemRender = ({ data }: any) => { - return

{data}

+ return
{data}
} const ItemRenderMemo = React.memo(ItemRender) const ItemVariable = ({ data, index }: any) => { return ( -

+

{data} -

+
) } const ItemVariableDemo = React.memo(ItemVariable) @@ -74,16 +107,13 @@ const ListDemo = () => { setIsLoading(false) }, 30) } - useEffect(() => { - getData() - }, [getData]) + const showNode = () => { switch (radioVal) { case '1': return ( { case '2': return ( - ) - case '3': - return ( - - ) - case '4': - return ( - ) default: return ( { > {translated.text1} {translated.text2} - {translated.text3} - {translated.text4} -
- {showNode()} -
+
{showNode()}
) diff --git a/src/packages/virtuallist/doc.taro.md b/src/packages/virtuallist/doc.taro.md index e1a0a71450b9ee4a2183345d2d706f78bfeb14d6..94457d22ca0f9c25fef7fd66fadd94bd87444969 100644 --- a/src/packages/virtuallist/doc.taro.md +++ b/src/packages/virtuallist/doc.taro.md @@ -15,83 +15,58 @@ import { VirtualList } from '@nutui/nutui-react-taro'; :::demo ``` tsx -import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { VirtualList } from '@nutui/nutui-react-taro'; const App =() => { const [sourceData, setsourceData] = useState([]) + const [pageNo, setPageNo] = useState(1) + const [isLoading, setIsLoading] = useState(false) + + const itemStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '50px', + background: '#fff', + borderRadius: '10px', + } const getData = useCallback(() => { const datas = [] const pageSize = 90 for (let i = 10; i < pageSize; i++) { - datas.push(`${i} Item`) + datas.push(`${i} Item`) } setsourceData((sourceData) => { - return [...sourceData, ...datas] + return [...sourceData, ...datas] }) }, []) + useEffect(() => { getData() }, [getData]) - const ItemRender = ({ data,index }) => { - return

自定义-{data}-{index}

+ + const ItemRender = ({ data }: any) => { + return
{data}
} const ItemRenderMemo = React.memo(ItemRender) - return ( -
- -
- ) -} -export default App; -``` -::: -### 2、垂直不等高&无限下滑 - -:::demo -``` tsx -import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' -import { VirtualList } from '@nutui/nutui-react-taro'; -const App =() => { - const [sourceData, setsourceData] = useState([]) - const [pageNo, setPageNo] = useState(1) - const getData = useCallback(() => { - const datas = [] - const pageSize = 90 - for (let i = 10; i < pageSize; i++) { - datas.push(`${i} Item`) - } - setsourceData((sourceData) => { - return [...sourceData, ...datas] - }) - }, []) - const onScroll = () => { - if (pageNo > 100) return - setPageNo(pageNo + 1) - } - useEffect(() => { - getData() - }, [getData]) - const ItemVariable = ({ data, index }) => { - return ( -

可变大小隔行展示-{data}

- ) + const onScroll = () => { + if (pageNo > 50 || isLoading) return + setIsLoading(true) + setTimeout(() => { + setPageNo(pageNo + 1) + setIsLoading(false) + }, 30) } - /** itemSize为首屏最大元素大小 */ - const ItemVariableDemo = React.memo(ItemVariable) return ( -
- +
@@ -100,91 +75,72 @@ const App =() => { export default App; ``` ::: - -### 3、水平等宽 +### 2、垂直不等高&无限下滑 :::demo ``` tsx -import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { VirtualList } from '@nutui/nutui-react-taro'; const App =() => { const [sourceData, setsourceData] = useState([]) const [pageNo, setPageNo] = useState(1) - const getData = useCallback(() => { - const datas = [] - const pageSize = 90 - for (let i = 10; i < pageSize; i++) { - datas.push(`${i} Item`) - } - setsourceData((sourceData) => { - return [...sourceData, ...datas] - }) - }, []) - useEffect(() => { - getData() - }, [getData]) - const ItemRender = ({ data,index }) => { - return

自定义-{data}-{index}

+ const [isLoading, setIsLoading] = useState(false) + + + const itemStyel2 = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + background: '#fff', + borderRadius: '10px', } - const ItemRenderMemo = React.memo(ItemRender) - return ( -
- -
- ) -} -export default App; -``` -::: -### 4、水平不等宽&无限滑动 - -:::demo -``` tsx -import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' -import { VirtualList } from '@nutui/nutui-react-taro'; -const App =() => { - const [sourceData, setsourceData] = useState([]) - const [pageNo, setPageNo] = useState(1) const getData = useCallback(() => { const datas = [] const pageSize = 90 for (let i = 10; i < pageSize; i++) { - datas.push(`${i} Item`) + datas.push(`${i} Item`) } setsourceData((sourceData) => { - return [...sourceData, ...datas] + return [...sourceData, ...datas] }) }, []) - const onScroll = () => { - if (pageNo > 100) return - setPageNo(pageNo + 1) - } + useEffect(() => { getData() }, [getData]) - const ItemVariable = ({ data, index }) => { + + const ItemVariable = ({ data, index }: any) => { return ( -

可变大小隔行展示-{data}

+
{data}
) } - /** itemSize为首屏最大元素大小 */ const ItemVariableDemo = React.memo(ItemVariable) + + const onScroll = () => { + if (pageNo > 50 || isLoading) return + setIsLoading(true) + setTimeout(() => { + setPageNo(pageNo + 1) + setIsLoading(false) + }, 30) + } return ( -
+
) @@ -192,6 +148,7 @@ const App =() => { export default App; ``` ::: + ## API ### Props @@ -199,17 +156,16 @@ export default App; | 参数 | 说明 | 类型 | 默认值 | |---------------|----------------------------------|----------|---------------------------------------| | sourceData | 获取数据 | Array | - | -| containerSize | 容器高度 | Number | 获取元素的 offsetWidth 或 offsetHeight,需要 css 给出 | +| containerSize | 容器高度 | Number | 获取元素的 offsetHeight,需要 css 给出 | | ItemRender | virtual 列表父节点渲染的函数 | React.FC | - | -| itemSize | item高度,如果不定高,则为首屏单个最大size | String | - | | itemEqualSize | item大小是否一致 | Boolean | true | +| itemSize | item高度,如果不定高,会走默认高度 | String | 66 | | overscan | 除了视窗里面默认的元素, 还需要额外渲染的item个数 | Number | 2 | | key | 唯一值 ,Item(sourceData)具体的某个唯一值的字段 | string | index | -| horizontal | 决定列表是横向的还是纵向的 | Boolean | false | + ## Events | 方法名 | 说明 | 参数 | 返回值 | |------------------|---------------------| --------------- | ---------- | -| handleScroll`废弃` | 滑动到底(右)的事件,可以实现无限滚动 | - | - | | onScroll`v1.3.8` | 滑动到底(右)的事件,可以实现无限滚动 | - | - | diff --git a/src/packages/virtuallist/virtuallist.scss b/src/packages/virtuallist/virtuallist.scss index 34f9d49bd910809213cbca0fb7261db3e294f5bd..ed574d42d40b180932354834d3df774443435567 100644 --- a/src/packages/virtuallist/virtuallist.scss +++ b/src/packages/virtuallist/virtuallist.scss @@ -61,3 +61,27 @@ } } } + +.nut-virtuallist { + width: 100%; + overflow: scroll; + position: relative; + -webkit-overflow-scrolling: touch; + &-phantom { + position: absolute; + left: 0; + top: 0; + right: 0; + z-index: -1; + } + &-container { + left: 0; + right: 0; + top: 0; + position: absolute; + } + &-item { + overflow: hidden; + margin: $list-item-margin; + } +} diff --git a/src/packages/virtuallist/virtuallist.taro.tsx b/src/packages/virtuallist/virtuallist.taro.tsx index 4bdf18caf909221973a4b48f686feccb8bcff19a..17000a60eb9c47ed9b5bee5ea92455720b265604 100644 --- a/src/packages/virtuallist/virtuallist.taro.tsx +++ b/src/packages/virtuallist/virtuallist.taro.tsx @@ -1,22 +1,38 @@ import React, { FunctionComponent, - useCallback, useEffect, useRef, useState, + useCallback, } from 'react' +import { ScrollView } from '@tarojs/components' +import Taro from '@tarojs/taro' import { useConfig } from '@/packages/configprovider/configprovider.taro' -import { BasicVirtualListProps, VirtualListState, PositionType } from './type' -import { - initPositinoCache, - getListTotalSize, - binarySearch, - getEndIndex, - updateItemSize, -} from './utils' - -export type VirtualListProps = BasicVirtualListProps -const defaultProps = {} as VirtualListProps +import { VirtualListState, PositionType } from './type' +import { initPositinoCache, binarySearch, updateItemSize } from './utils' + +export type VirtualListProps = { + className?: string | any + style?: React.CSSProperties + sourceData: any // 获取数据 + containerSize?: number // 容器大小 + ItemRender?: any // virtual 列表父节点渲染的函数,默认为 (items, ref) =>
{items}
+ itemEqualSize?: boolean // item 固定大小,默认是true + itemSize?: number // 预估元素高度 + overscan?: number // 除了视窗里面默认的元素, 还需要额外渲染的, 避免滚动过快, 渲染不及时,默认是2 + onScroll?: (...args: any[]) => any // 滑动到底部执行的函数 + key?: any // 遍历时生成item 的唯一key,默认是index,建议传入resources的数据具体的某个唯一值的字段 + locale?: any +} +const defaultProps = { + sourceData: [], + itemSize: 66, + itemEqualSize: true, + overscan: 2, +} as VirtualListProps + +const clientHeight = Taro.getSystemInfoSync().windowHeight - 5 || 667 +const clientWidth = Taro.getSystemInfoSync().windowWidth || 375 export const VirtualList: FunctionComponent< VirtualListProps & React.HTMLAttributes @@ -24,27 +40,29 @@ export const VirtualList: FunctionComponent< const { sourceData = [], ItemRender, + itemSize = 66, itemEqualSize = true, - itemSize = 200, - horizontal = false, overscan = 2, key, - handleScroll, onScroll, className, - containerSize, + containerSize = clientHeight, ...rest } = props - const sizeKey = horizontal ? 'width' : 'height' - const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop' - const offsetKey = horizontal ? 'left' : 'top' + // const sizeKey = horizontal ? 'width' : 'height' + // const scrollKey = horizontal ? 'scrollLeft' : 'scrollTop' + // const offsetKey = horizontal ? 'left' : 'top' + + const [startOffset, setStartOffset] = useState(0) + const [start, setStart] = useState(0) + const [list, setList] = useState(sourceData.slice()) const { locale } = useConfig() // 虚拟列表容器ref const scrollRef = useRef(null) // 虚拟列表显示区域ref - const itemsRef = useRef(null) - const firstItemRef = useRef(null) + const itemsRef = useRef(null) + const firstItemRef = useRef(null) // 列表位置信息 const [positions, setPositions] = useState([ { @@ -57,10 +75,7 @@ export const VirtualList: FunctionComponent< right: 0, }, ]) - // 列表总大小 - const [listTotalSize, setListTotalSize] = useState(99999999) - // 可视区域条数 - const [visibleCount, setVisibleCount] = useState(0) + const [offSetSize, setOffSetSize] = useState(containerSize || 0) const [options, setOptions] = useState({ startOffset: 0, // 可视区域距离顶部的偏移量 @@ -69,97 +84,88 @@ export const VirtualList: FunctionComponent< endIndex: 10, // 可视区域结束索引 }) - // 列表位置信息 useEffect(() => { - const pos = initPositinoCache(itemSize, sourceData.length) - setPositions(pos) - const totalSize = getListTotalSize(pos, horizontal) - setListTotalSize(totalSize) - }, [sourceData, itemSize, horizontal]) - const getElement = useCallback(() => { - return scrollRef.current?.parentElement || document.body - }, []) + if (sourceData.length) { + setList(sourceData.slice()) + } + }, [sourceData]) + + // 初始计算可视区域展示数量 + useEffect(() => { + setPositions((options) => { + return { ...options, endIndex: visibleCount() } + }) + }, [itemSize, overscan, offSetSize]) + useEffect(() => { if (containerSize) return - const size = horizontal - ? getElement().offsetWidth - : getElement().offsetHeight - setOffSetSize(size) - }, [getElement, horizontal, containerSize]) + + setOffSetSize(getContainerHeight()) + }, [containerSize]) + useEffect(() => { - // 初始-计算visibleCount - if (offSetSize === 0) return - const count = Math.ceil(offSetSize / itemSize) + overscan + const pos = initPositinoCache(itemSize, sourceData.length) - setVisibleCount(count) - setOptions((options) => { - return { ...options, endIndex: count } - }) - }, [getElement, horizontal, itemSize, overscan, offSetSize]) + setPositions(pos) + }, [itemSize, sourceData]) + + // 可视区域总高度 + const getContainerHeight = () => { + //初始首页列表高度 + const initH = itemSize * sourceData.length + //未设置containerSize高度,判断首页高度小于设备高度时,滚动容器高度为首页数据高度,减5为分页触发的偏移量 + let containerH = + initH < clientHeight + ? initH + overscan * itemSize - 5 + : Math.min(containerSize, clientHeight) + + return containerH // Math.min(containerSize, clientHeight) + } + // 可视区域条数 + const visibleCount = () => { + return Math.ceil(getContainerHeight() / itemSize) + overscan + } + + const end = () => { + return start + visibleCount() + } + + const listHeight = () => { + return list.length * itemSize + } + + const visibleData = () => { + return list.slice(start, Math.min(end(), list.length)) + } const updateTotalSize = useCallback(() => { if (!itemsRef.current) return const items: HTMLCollection = itemsRef.current.children if (!items.length) return // 更新缓存 - updateItemSize(positions, items, sizeKey) - const totalSize = getListTotalSize(positions, horizontal) - setListTotalSize(totalSize) - }, [positions, sizeKey, horizontal]) - - const scroll = useCallback(() => { - requestAnimationFrame((e) => { - const scrollSize = getElement()[scrollKey] - const startIndex = binarySearch(positions, scrollSize, horizontal) - const overStart = startIndex - overscan > -1 ? startIndex - overscan : 0 - // const offSetSize = horizontal ? getElement().offsetWidth : getElement().offsetHeight - if (!itemEqualSize) { - updateTotalSize() - } - const endIndex = getEndIndex({ - sourceData, - startIndex, - visibleCount, - itemEqualSize, - positions, - offSetSize, - sizeKey, - overscan, - }) - const startOffset = positions[startIndex][offsetKey] as number - setOptions({ startOffset, startIndex, overStart, endIndex }) - // 无限下滑 - if (endIndex > sourceData.length - 1) { - if (onScroll) { - onScroll() - } else if (handleScroll) { - handleScroll() - } - } - }) - }, [ - positions, - getElement, - sourceData, - visibleCount, - itemEqualSize, - updateTotalSize, - offsetKey, - sizeKey, - scrollKey, - horizontal, - overscan, - handleScroll, - offSetSize, - ]) + updateItemSize(positions, items, 'height') + }, [positions]) - useEffect(() => { - const element = getElement() - element.addEventListener('scroll', scroll, false) - return () => { - element.removeEventListener('scroll', scroll, false) + // 滚动监听 + const listScroll = (e: any) => { + const scrollTop = e.detail ? e.detail.scrollTop : e.target.scrollTop + const scrollSize = Math.floor(scrollTop) + const startIndex = binarySearch(positions, scrollSize, false) + + const overStart = startIndex - overscan > -1 ? startIndex - overscan : 0 + const endIndex = end() + if (!itemEqualSize) { + updateTotalSize() + } + setStart(Math.floor(scrollTop / itemSize)) + + setOptions({ startOffset, startIndex, overStart, endIndex }) + + if (end() > list.length - 1) { + onScroll && onScroll() } - }, [getElement, scroll]) + setStartOffset(scrollTop - (scrollTop % itemSize)) + } return (
-
-
    +
    - {sourceData - .slice(options.overStart, options.endIndex) - .map((data, index) => { - const { startIndex, overStart } = options - const dataIndex = overStart + index - const styleVal = dataIndex < startIndex ? 'none' : 'block' - const keyVal = key && data[key] ? data[key] : dataIndex - - return ( -
  • - {ItemRender ? ( - - ) : ( - data - )} -
  • - ) - })} -
-
+ {visibleData().map((data: any, index: number) => { + const { overStart } = options + const dataIndex = overStart + index + const keyVal = key && data[key] ? data[key] : dataIndex + return ( +
+ {ItemRender ? ( + + ) : ( + data + )} +
+ ) + })} +
+
) } diff --git a/src/sites/mobile-taro/src/app.config.ts b/src/sites/mobile-taro/src/app.config.ts index ae4f70ba0589329684356788a0081d3755f177c0..003674e4f874fcc7f00f243366be8ae41090cc22 100644 --- a/src/sites/mobile-taro/src/app.config.ts +++ b/src/sites/mobile-taro/src/app.config.ts @@ -87,6 +87,7 @@ const subPackages = [ 'pages/table/index', 'pages/tag/index', 'pages/trendarrow/index', + 'pages/virtuallist/index', 'pages/watermark/index', ], },