未验证 提交 3f6ea9c5 编写于 作者: V vickyYe 提交者: GitHub

feat: 虚拟列表 virtuallist 组件适配 taro (#602)

上级 487994af
......@@ -1039,8 +1039,9 @@
"cName": "虚拟列表",
"desc": "长列表渲染",
"sort": 20,
"tarodoc":true,
"show": true,
"taro": false,
"taro": true,
"author": "hx"
},
{
......
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
......
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 <p>{data}</p>
return <div style={itemStyle}>{data}</div>
}
const ItemRenderMemo = React.memo(ItemRender)
const ItemVariable = ({ data, index }: any) => {
return (
<p className={index % 2 === 0 ? '' : 'nut-virtualList-demo-item'}>
<div
style={{
height: `${index % 2 === 0 ? '100px' : '50px'}`,
...itemStyel2,
}}
>
{data}
</p>
</div>
)
}
const ItemVariableDemo = React.memo(ItemVariable)
......@@ -74,16 +107,13 @@ const ListDemo = () => {
setIsLoading(false)
}, 30)
}
useEffect(() => {
getData()
}, [getData])
const showNode = () => {
switch (radioVal) {
case '1':
return (
<VirtualList
itemSize={66}
className="heigh1"
itemSize={50}
sourceData={sourceData}
ItemRender={ItemRenderMemo}
onScroll={onScroll}
......@@ -92,41 +122,18 @@ const ListDemo = () => {
case '2':
return (
<VirtualList
itemSize={80}
sourceData={sourceData}
ItemRender={ItemVariableDemo}
itemSize={128}
containerSize={500}
itemEqualSize={false}
onScroll={onScroll}
/>
)
case '3':
return (
<VirtualList
sourceData={sourceData}
ItemRender={ItemRenderMemo}
itemSize={124}
containerSize={341}
onScroll={onScroll}
horizontal
/>
)
case '4':
return (
<VirtualList
sourceData={sourceData}
itemSize={300}
ItemRender={ItemVariableDemo}
horizontal
itemEqualSize={false}
onScroll={onScroll}
containerSize={500}
/>
)
default:
return (
<VirtualList
itemSize={66}
className="heigh1"
itemSize={50}
sourceData={sourceData}
ItemRender={ItemRenderMemo}
onScroll={onScroll}
......@@ -147,14 +154,10 @@ const ListDemo = () => {
>
<Radio value="1">{translated.text1}</Radio>
<Radio value="2">{translated.text2}</Radio>
<Radio value="3">{translated.text3}</Radio>
<Radio value="4">{translated.text4}</Radio>
</RadioGroup>
</Cell>
</CellGroup>
<div key={radioVal} className="nut-virtualList-demo-box hideScrollbar">
{showNode()}
</div>
<div style={{ height: '100%' }}>{showNode()}</div>
</div>
</>
)
......
......@@ -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 <p>自定义-{data}-{index}</p>
const ItemRender = ({ data }: any) => {
return <div style={itemStyle}>{data}</div>
}
const ItemRenderMemo = React.memo(ItemRender)
return (
<div className='nut-virtualList-demo-box hideScrollbar heigh1'>
<VirtualList
itemSize={66}
sourceData={sourceData}
ItemRender={ItemRenderMemo}
/>
</div>
)
}
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 (
<p className={index % 2 === 0 ? '' : 'nut-virtualList-demo-item'}>可变大小隔行展示-{data}</p>
)
const onScroll = () => {
if (pageNo > 50 || isLoading) return
setIsLoading(true)
setTimeout(() => {
setPageNo(pageNo + 1)
setIsLoading(false)
}, 30)
}
/** itemSize为首屏最大元素大小 */
const ItemVariableDemo = React.memo(ItemVariable)
return (
<div className='nut-virtualList-demo-box hideScrollbar heigh1'>
<VirtualList
<div style={{ height: '100%' }}>
<VirtualList
itemSize={50}
sourceData={sourceData}
ItemRender={ItemVariableDemo}
itemSize={128}
itemEqualSize={false}
ItemRender={ItemRenderMemo}
onScroll={onScroll}
/>
</div>
......@@ -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 <p>自定义-{data}-{index}</p>
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 (
<div className='nut-virtualList-demo-box hideScrollbar'>
<VirtualList
sourceData={sourceData}
ItemRender={ItemRenderMemo}
itemSize={124}
horizontal
/>
</div>
)
}
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 (
<p className={index % 2 === 0 ? '' : 'nut-virtualList-demo-item'}>可变大小隔行展示-{data}</p>
<div
style={{
height: `${index % 2 === 0 ? '100px' : '50px'}`,
...itemStyel2,
}}
>{data}</div>
)
}
/** itemSize为首屏最大元素大小 */
const ItemVariableDemo = React.memo(ItemVariable)
const onScroll = () => {
if (pageNo > 50 || isLoading) return
setIsLoading(true)
setTimeout(() => {
setPageNo(pageNo + 1)
setIsLoading(false)
}, 30)
}
return (
<div className='nut-virtualList-demo-box hideScrollbar'>
<div style={{ height: '100%' }}>
<VirtualList
itemSize={80}
sourceData={sourceData}
itemSize={300}
ItemRender={ItemVariableDemo}
horizontal
itemEqualSize={false}
onScroll={onScroll}
itemEqualSize={false}
containerSize={500}
/>
</div>
)
......@@ -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` | 滑动到底(右)的事件,可以实现无限滚动 | - | - |
......
......@@ -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;
}
}
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) => <div ref={ref}>{items}</div>
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<HTMLDivElement>
......@@ -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<HTMLDivElement>(null)
// 虚拟列表显示区域ref
const itemsRef = useRef<HTMLUListElement>(null)
const firstItemRef = useRef<HTMLLIElement>(null)
const itemsRef = useRef<HTMLDivElement>(null)
const firstItemRef = useRef(null)
// 列表位置信息
const [positions, setPositions] = useState<PositionType[]>([
{
......@@ -57,10 +75,7 @@ export const VirtualList: FunctionComponent<
right: 0,
},
])
// 列表总大小
const [listTotalSize, setListTotalSize] = useState<number>(99999999)
// 可视区域条数
const [visibleCount, setVisibleCount] = useState<number>(0)
const [offSetSize, setOffSetSize] = useState<number>(containerSize || 0)
const [options, setOptions] = useState<VirtualListState>({
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 (
<div
......@@ -168,60 +174,52 @@ export const VirtualList: FunctionComponent<
}
{...rest}
style={{
[sizeKey]: containerSize ? `${offSetSize}px` : '',
height: containerSize ? `${offSetSize}px` : '',
}}
>
<div
<ScrollView
scrollTop={0}
scrollY
ref={scrollRef}
className={horizontal ? 'nut-horizontal-box' : 'nut-vertical-box'}
className="nut-virtuallist"
style={{
position: 'relative',
[sizeKey]: `${listTotalSize}px`,
height: `${getContainerHeight()}px`,
}}
onScroll={listScroll}
>
<ul
className={
horizontal
? 'nut-virtuallist-items nut-horizontal-items'
: 'nut-virtuallist-items nut-vertical-items'
}
<div
className="nut-virtuallist-phantom"
style={{ height: `${listHeight()}px` }}
/>
<div
className="nut-virtuallist-container"
ref={itemsRef}
style={{
transform: horizontal
? `translate3d(${options.startOffset}px,0,0)`
: `translate3d(0,${options.startOffset}px,0)`,
}}
style={{ transform: `translate3d(0, ${startOffset}px, 0)` }}
>
{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 (
<li
ref={dataIndex === 0 ? firstItemRef : null}
data-index={`${dataIndex}`}
className={
dataIndex % 2 === 0
? 'nut-virtuallist-item even'
: 'nut-virtuallist-item odd'
}
key={`${keyVal}`}
style={{ display: styleVal }}
>
{ItemRender ? (
<ItemRender data={data} index={`${dataIndex}`} />
) : (
data
)}
</li>
)
})}
</ul>
</div>
{visibleData().map((data: any, index: number) => {
const { overStart } = options
const dataIndex = overStart + index
const keyVal = key && data[key] ? data[key] : dataIndex
return (
<div
ref={dataIndex === 0 ? firstItemRef : null}
data-index={`${dataIndex}`}
className="nut-virtuallist-item"
key={`${data}`}
style={{
height: `${itemEqualSize ? `${itemSize}px` : 'auto'}`,
}}
>
{ItemRender ? (
<ItemRender data={data} index={`${index}`} />
) : (
data
)}
</div>
)
})}
</div>
</ScrollView>
</div>
)
}
......
......@@ -87,6 +87,7 @@ const subPackages = [
'pages/table/index',
'pages/tag/index',
'pages/trendarrow/index',
'pages/virtuallist/index',
'pages/watermark/index',
],
},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册