Skip to content

Commit

Permalink
feat: 新增虚拟瀑布流组件
Browse files Browse the repository at this point in the history
  • Loading branch information
ZakaryCode committed Jul 13, 2023
1 parent 11d68b4 commit 475c404
Show file tree
Hide file tree
Showing 24 changed files with 1,281 additions and 172 deletions.
1 change: 1 addition & 0 deletions packages/taro-components-advanced/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@tarojs/shared": "workspace:*",
"@tarojs/runtime": "workspace:*",
"@tarojs/taro": "workspace:*",
"classnames": "^2.2.5",
"memoize-one": "^6.0.0",
"postcss": "^8.4.18"
},
Expand Down
5 changes: 4 additions & 1 deletion packages/taro-components-advanced/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export default {
'src/components/index.ts',
'src/components/virtual-list/index.ts',
'src/components/virtual-list/react/index.ts',
'src/components/virtual-list/vue/index.ts'
'src/components/virtual-list/vue/index.ts',
'src/components/virtual-waterfall/index.ts',
'src/components/virtual-waterfall/react/index.ts',
'src/components/virtual-waterfall/vue/index.ts',
],
output: {
dir: 'dist',
Expand Down
1 change: 1 addition & 0 deletions packages/taro-components-advanced/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './virtual-list'
export * from './virtual-waterfall'
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { BaseEventOrig, BaseEventOrigFunction, ScrollViewProps, StandardProps } from '@tarojs/components'
import type { Component, ComponentType, CSSProperties, ReactNode } from 'react'

interface VirtualListProps extends Omit<StandardProps, 'children'> {
interface VirtualListProps<T = any> extends Omit<StandardProps, 'children'> {
/** 列表的高度。 */
height: string | number
/** 列表的宽度。 */
Expand All @@ -13,7 +13,7 @@ interface VirtualListProps extends Omit<StandardProps, 'children'> {
/** 单项的样式,样式必须传入组件的 style 中 */
style?: CSSProperties
/** 组件渲染的数据 */
data: any
data: T[]
/** 组件渲染数据的索引 */
index: number
/** 组件是否正在滚动,当 useIsScrolling 值为 true 时返回布尔值,否则返回 undefined */
Expand All @@ -22,14 +22,14 @@ interface VirtualListProps extends Omit<StandardProps, 'children'> {
/** 列表的长度 */
itemCount: number
/** 渲染数据 */
itemData: any[]
itemData: T[]
/** 列表单项的大小,垂直滚动时为高度,水平滚动时为宽度。
*
* > Note:
* > - unlimitedSize 模式下如果传入函数,只会调用一次用于设置初始值
* > - 非 unlimitedSize 模式下如果传入函数,为避免性能问题,每个节点只会调用一次用于设置初始值
*/
itemSize: number | ((index?: number, itemData?: any[]) => number)
itemSize: number | ((index?: number, itemData?: T[]) => number)
/** 解开高度列表单项大小限制,默认值使用: itemSize (请注意,初始高度与实际高度差异过大会导致隐患)。
*
* > Note: 通过 itemSize 设置的初始高度与子节点实际高度差异过大会导致隐患
Expand All @@ -40,35 +40,43 @@ interface VirtualListProps extends Omit<StandardProps, 'children'> {
* @default "absolute"
*/
position?: 'absolute' | 'relative'
/** 滚动方向。vertical 为垂直滚动,horizontal 为平行滚动。
* @default "vertical"
*/
layout?: 'vertical' | 'horizontal'
/** 初始滚动偏移值,水平滚动影响 scrollLeft,垂直滚动影响 scrollTop。 */
initialScrollOffset?: number
/** 列表内部容器组件类型。
* @default View
*/
innerElementType?: ComponentType
/** 在可视区域之外渲染的列表单项数量,值设置得越高,快速滚动时出现白屏的概率就越小,相应地,每次滚动的性能会变得越差。 */
overscanCount?: number
/** 上下滚动预占位节点 */
placeholderCount?: number
/** 是否注入 isScrolling 属性到 item 组件。这个参数一般用于实现滚动骨架屏(或其它 placeholder) 时比较有用。 */
useIsScrolling?: boolean
/** 通过 ScrollViewContext 优化组件滚动性能
* @default false
* @note 部分平台不支持,使用时请注意甄别
*/
enhanced?: boolean
/** 列表外部容器组件类型。
* @default ScrollView
*/
outerElementType?: ComponentType | string
/** 列表内部容器组件类型。
* @default View
*/
innerElementType?: ComponentType | string
/** 列表子节点容器组件类型。
* @default View
*/
itemElementType?: ComponentType | string
/** 顶部区域 */
renderTop?: ReactNode
/** 底部区域 */
renderBottom?: ReactNode
/** 滚动方向。vertical 为垂直滚动,horizontal 为平行滚动。
* @default "vertical"
*/
layout?: 'vertical' | 'horizontal'
/** 列表滚动时调用函数 */
onScroll?: (event: VirtualListProps.IVirtualListEvent<VirtualListProps.IVirtualListEventDetail>) => void
/** 调用平台原生的滚动监听函数。 */
onScrollNative?: BaseEventOrigFunction<ScrollViewProps.onScrollDetail>
/** 在可视区域之外渲染的列表单项数量,值设置得越高,快速滚动时出现白屏的概率就越小,相应地,每次滚动的性能会变得越差。 */
overscanCount?: number
/** 上下滚动预占位节点 */
placeholderCount?: number
/** 是否注入 isScrolling 属性到 item 组件。这个参数一般用于实现滚动骨架屏(或其它 placeholder) 时比较有用。 */
useIsScrolling?: boolean
style?: CSSProperties
}

Expand All @@ -78,6 +86,9 @@ declare namespace VirtualListProps {
scrollTop: number
scrollHeight: number
scrollWidth: number
clientHeight: number
clientWidth: number
diffOffset: number
}

interface IVirtualListEvent<T extends ScrollViewProps.onScrollDetail = ScrollViewProps.onScrollDetail> extends BaseEventOrig {
Expand All @@ -87,7 +98,7 @@ declare namespace VirtualListProps {
scrollOffset: number
/** 当滚动是由 scrollTo() 或 scrollToItem() 调用时返回 true,否则返回 false */
scrollUpdateWasRequested: boolean
/** 当前只有 React 支持 */
/** 滚动信息 */
detail: T
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isFunction } from '@tarojs/shared'

import { getOffsetForIndexAndAlignment } from '../../utils'
import { isHorizontalFunc } from './utils'

import type { IProps } from './preset'
Expand Down Expand Up @@ -147,52 +148,14 @@ export default class ListSet {
}

getOffsetForIndexAndAlignment (index: number, align: string, scrollOffset: number) {
const wrapperSize = this.wrapperSize
const itemSize = this.getSize(index)
const lastItemOffset = Math.max(0, this.getOffsetSize(this.props.itemCount) - wrapperSize)
const maxOffset = Math.min(lastItemOffset, this.getOffsetSize(index))
const minOffset = Math.max(0, this.getOffsetSize(index) - wrapperSize + itemSize)

if (align === 'smart') {
if (scrollOffset >= minOffset - wrapperSize && scrollOffset <= maxOffset + wrapperSize) {
align = 'auto'
} else {
align = 'center'
}
}

switch (align) {
case 'start':
return maxOffset

case 'end':
return minOffset

case 'center':
{
// "Centered" offset is usually the average of the min and max.
// But near the edges of the list, this doesn't hold true.
const middleOffset = Math.round(minOffset + (maxOffset - minOffset) / 2)

if (middleOffset < Math.ceil(wrapperSize / 2)) {
return 0 // near the beginning
} else if (middleOffset > lastItemOffset + Math.floor(wrapperSize / 2)) {
return lastItemOffset // near the end
} else {
return middleOffset
}
}

case 'auto':
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset
} else if (scrollOffset < minOffset) {
return minOffset
} else {
return maxOffset
}
}
return getOffsetForIndexAndAlignment({
align,
containerSize: this.wrapperSize,
currentOffset: scrollOffset,
scrollSize: this.getOffsetSize(this.props.itemCount),
slideSize: this.getSize(index),
targetOffset: this.getOffsetSize(index),
})
}

compareSize (i = 0, size = 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import memoizeOne from 'memoize-one'

import { convertNumber2PX, isCosDistributing } from '../../utils'
import { convertNumber2PX, defaultItemKey, isCosDistributing } from '../../utils'
import ListSet from './list-set'
import { defaultItemKey, isHorizontalFunc, isRtlFunc } from './utils'
import { isHorizontalFunc, isRtlFunc } from './utils'

import type { VirtualListProps } from './'

Expand All @@ -15,8 +15,6 @@ export interface IProps extends Partial<VirtualListProps> {
itemTagName?: string
innerTagName?: string
outerTagName?: string
itemElementType?: React.ComponentType | string
outerElementType?: React.ComponentType | string
innerRef?: React.Ref<HTMLElement> | string
outerRef?: React.Ref<HTMLElement> | string
onItemsRendered?: TFunc
Expand Down Expand Up @@ -72,15 +70,15 @@ export default class Preset {
return this.props.placeholderCount >= 0 ? this.props.placeholderCount : this.props.overscanCount
}

get outerTagName () {
get outerElement () {
return this.props.outerElementType || this.props.outerTagName || 'div'
}

get innerTagName () {
get innerElement () {
return this.props.innerElementType || this.props.innerTagName || 'div'
}

get itemTagName () {
get itemElement () {
return this.props.itemElementType || this.props.itemTagName || 'div'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ const VirtualList = React.forwardRef(function VirtualList (props: VirtualListPro
return React.createElement(List, {
ref,
...rest,
outerElementType: OuterScrollView,
itemElementType,
innerElementType,
outerElementType: OuterScrollView,
direction,
initialScrollOffset,
overscanCount
overscanCount,
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import memoizeOne from 'memoize-one'
import React from 'react'

import { convertNumber2PX } from '../../../utils/convert'
import { omit } from '../../../utils/lodash'
import { cancelTimeout, requestTimeout } from '../../../utils/timer'
import { cancelTimeout, convertNumber2PX, defaultItemKey, getRectSize, getScrollViewContextNode, omit, requestTimeout } from '../../../utils'
import { IS_SCROLLING_DEBOUNCE_INTERVAL } from '../constants'
import { getRTLOffsetType } from '../dom-helpers'
import ListSet from '../list-set'
import Preset from '../preset'
import { defaultItemKey, getRectSize, getScrollViewContextNode } from '../utils'
import { validateListProps } from './validate'

import type { IProps } from '../preset'
Expand Down Expand Up @@ -45,7 +42,7 @@ export default class List extends React.PureComponent<IProps, IState> {

this.preset = new Preset(
props,
this.refresh
this.refresh,
)
this.itemList = this.preset.itemList

Expand All @@ -59,7 +56,7 @@ export default class List extends React.PureComponent<IProps, IState> {
? this.props.initialScrollOffset
: 0,
scrollUpdateWasRequested: false,
refreshCount: 0
refreshCount: 0,
}
}

Expand All @@ -77,11 +74,11 @@ export default class List extends React.PureComponent<IProps, IState> {

_resetIsScrollingTimeoutId = null

_callOnItemsRendered = memoizeOne((overscanStartIndex, overscanStopIndex, visibleStartIndex, visibleStopIndex) => this.props.onItemsRendered({
_callOnItemsRendered = memoizeOne((overscanStartIndex, overscanStopIndex, startIndex, stopIndex) => this.props.onItemsRendered({
overscanStartIndex,
overscanStopIndex,
visibleStartIndex,
visibleStopIndex
startIndex,
stopIndex
}))

_callOnScroll = memoizeOne((scrollDirection, scrollOffset, scrollUpdateWasRequested, detail) => this.props.onScroll({
Expand All @@ -95,9 +92,9 @@ export default class List extends React.PureComponent<IProps, IState> {
if (typeof this.props.onItemsRendered === 'function') {
if (this.props.itemCount > 0) {
if (prevProps && prevProps.itemCount !== this.props.itemCount) {
const [overscanStartIndex, overscanStopIndex, visibleStartIndex, visibleStopIndex] = this._getRangeToRender()
const [overscanStartIndex, overscanStopIndex, startIndex, stopIndex] = this._getRangeToRender()

this._callOnItemsRendered(overscanStartIndex, overscanStopIndex, visibleStartIndex, visibleStopIndex)
this._callOnItemsRendered(overscanStartIndex, overscanStopIndex, startIndex, stopIndex)
}
}
}
Expand Down Expand Up @@ -423,14 +420,14 @@ export default class List extends React.PureComponent<IProps, IState> {
if (itemCount > 0) {
const prevPlaceholder = startIndex < placeholderCount ? startIndex : placeholderCount
items.push(new Array(prevPlaceholder).fill(-1).map((_, index) => React.createElement<any>(
this.preset.itemTagName, {
this.preset.itemElement, {
key: itemKey(index + startIndex - prevPlaceholder, itemData),
style: { display: 'none' }
}
)))
for (let index = startIndex; index <= stopIndex; index++) {
const style = this.preset.getItemStyle(index)
items.push(React.createElement<any>(this.preset.itemTagName, {
items.push(React.createElement<any>(this.preset.itemElement, {
key: itemKey(index, itemData),
style
}, React.createElement(item, {
Expand All @@ -444,7 +441,7 @@ export default class List extends React.PureComponent<IProps, IState> {
restCount = restCount > 0 ? restCount : 0
const postPlaceholder = restCount < placeholderCount ? restCount : placeholderCount
items.push(new Array(postPlaceholder).fill(-1).map((_, index) => React.createElement<any>(
this.preset.itemTagName, {
this.preset.itemElement, {
key: itemKey(1 + index + stopIndex, itemData),
style: { display: 'none' }
}
Expand All @@ -454,7 +451,7 @@ export default class List extends React.PureComponent<IProps, IState> {
// Read this value AFTER items have been created,
// So their actual sizes (if variable) are taken into consideration.
const estimatedTotalSize = convertNumber2PX(this.itemList.getOffsetSize())
const outerElementProps: any = {
const outerProps: any = {
...rest,
id,
className,
Expand All @@ -476,25 +473,25 @@ export default class List extends React.PureComponent<IProps, IState> {

if (!enhanced) {
if (isHorizontal) {
outerElementProps.scrollLeft = scrollUpdateWasRequested ? scrollOffset : this.preset.field.scrollLeft
outerProps.scrollLeft = scrollUpdateWasRequested ? scrollOffset : this.preset.field.scrollLeft
} else {
outerElementProps.scrollTop = scrollUpdateWasRequested ? scrollOffset : this.preset.field.scrollTop
outerProps.scrollTop = scrollUpdateWasRequested ? scrollOffset : this.preset.field.scrollTop
}
}

if (this.preset.isRelative) {
const pre = convertNumber2PX(this.itemList.getOffsetSize(startIndex))
return React.createElement(this.preset.outerTagName, outerElementProps,
return React.createElement(this.preset.outerElement, outerProps,
renderTop,
React.createElement<any>(this.preset.itemTagName, {
React.createElement<any>(this.preset.itemElement, {
key: `${id}-pre`,
id: `${id}-pre`,
style: {
height: isHorizontal ? '100%' : pre,
width: !isHorizontal ? '100%' : pre
}
}),
React.createElement<any>(this.preset.innerTagName, {
React.createElement<any>(this.preset.innerElement, {
ref: innerRef,
key: `${id}-inner`,
id: `${id}-inner`,
Expand All @@ -506,9 +503,9 @@ export default class List extends React.PureComponent<IProps, IState> {
renderBottom
)
} else {
return React.createElement(this.preset.outerTagName, outerElementProps,
return React.createElement(this.preset.outerElement, outerProps,
renderTop,
React.createElement<any>(this.preset.innerTagName, {
React.createElement<any>(this.preset.innerElement, {
ref: innerRef,
key: `${id}-inner`,
id: `${id}-inner`,
Expand Down
Loading

0 comments on commit 475c404

Please sign in to comment.