Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/components/picker-item/picker-item.less
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

&__wrapper {
padding: 144rpx 0;
// 虚拟滚动性能优化:使用 will-change 提示浏览器
will-change: transform;
}

&__item {
Expand All @@ -30,6 +32,8 @@
align-items: center;
color: @picker-item-color;
font-size: @picker-item-font-size;
// 虚拟滚动性能优化:隔离渲染上下文
contain: layout style paint;

&-icon {
font-size: 36rpx;
Expand Down
263 changes: 243 additions & 20 deletions packages/components/picker-item/picker-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ const INERTIA_TIME = 300;
// 且距离大于`MOMENTUM_DISTANCE`时,执行惯性滚动
const INERTIA_DISTANCE = 15;

// 虚拟滚动配置
const VIRTUAL_SCROLL_CONFIG = {
ENABLE_THRESHOLD: 100, // 超过100个选项启用虚拟滚动
VISIBLE_COUNT: 5, // 可见区域显示5个选项
BUFFER_COUNT: 8, // 上下各缓冲8个选项(增加缓冲区,防止快速滑动时空白)
THROTTLE_TIME: 16, // 节流时间(60fps,提高更新频率)
FAST_SCROLL_BUFFER: 12, // 快速滑动时的额外缓冲区
FAST_SCROLL_THRESHOLD: 50, // 判定为快速滑动的速度阈值(px/frame)
};

const range = function (num: number, min: number, max: number) {
return Math.min(Math.max(num, min), max);
};
Expand Down Expand Up @@ -70,13 +80,35 @@ export default class PickerItem extends SuperComponent {
columnIndex: 0,
pickerKeys: { value: 'value', label: 'label', icon: 'icon' },
formatOptions: props.options.value,
// 虚拟滚动相关
enableVirtualScroll: false, // 是否启用虚拟滚动
visibleOptions: [], // 可见的选项列表
virtualStartIndex: 0, // 虚拟滚动起始索引
virtualOffsetY: 0, // 虚拟滚动偏移量
totalHeight: 0, // 总高度(用于占位)
};

lifetimes = {
created() {
this.StartY = 0;
this.StartOffset = 0;
this.startTime = 0;
this._moveTimer = null;
this._animationTimer = null; // 动画期间更新虚拟滚动的定时器
this._lastOffset = 0; // 上一次的偏移量(用于计算滑动速度)
this._lastMoveTime = 0; // 上一次移动的时间
this._scrollDirection = 0; // 滑动方向:1向下,-1向上,0静止
},
detached() {
// 清理定时器,防止内存泄漏
if (this._moveTimer) {
clearTimeout(this._moveTimer);
this._moveTimer = null;
}
if (this._animationTimer) {
clearInterval(this._animationTimer);
this._animationTimer = null;
}
},
};

Expand Down Expand Up @@ -104,16 +136,60 @@ export default class PickerItem extends SuperComponent {

onTouchMove(event) {
const { StartY, StartOffset } = this;
const { pickItemHeight } = this.data;
const { pickItemHeight, enableVirtualScroll } = this.data;
const currentTime = Date.now();

// 偏移增量
const deltaY = event.touches[0].clientY - StartY;
const newOffset = range(StartOffset + deltaY, -(this.getCount() * pickItemHeight), 0);
this.setData({
offset: newOffset,
});

// 计算滑动速度和方向
const offsetDelta = newOffset - this._lastOffset;
const timeDelta = currentTime - this._lastMoveTime || 16;
const scrollSpeed = Math.abs(offsetDelta / timeDelta) * 16; // 转换为 px/frame

// 计算滑动方向(避免嵌套三元表达式)
if (offsetDelta > 0) {
this._scrollDirection = 1; // 向下滑动
} else if (offsetDelta < 0) {
this._scrollDirection = -1; // 向上滑动
} else {
this._scrollDirection = 0; // 静止
}

// 判断是否为快速滑动
const isFastScroll = scrollSpeed > VIRTUAL_SCROLL_CONFIG.FAST_SCROLL_THRESHOLD;

// 优化节流策略:快速滑动时立即更新,慢速滑动时节流
if (!this._moveTimer || isFastScroll) {
if (this._moveTimer) {
clearTimeout(this._moveTimer);
this._moveTimer = null;
}

this.setData({ offset: newOffset });

// 虚拟滚动:更新可见范围(快速滑动时使用更大的缓冲区)
if (enableVirtualScroll) {
this.updateVisibleOptions(newOffset, isFastScroll);
}

this._moveTimer = setTimeout(() => {
this._moveTimer = null;
}, VIRTUAL_SCROLL_CONFIG.THROTTLE_TIME);
}

// 记录当前状态
this._lastOffset = newOffset;
this._lastMoveTime = currentTime;
},

onTouchEnd(event) {
if (this._moveTimer) {
clearTimeout(this._moveTimer);
this._moveTimer = null;
}

const { offset, pickItemHeight } = this.data;
const { startTime } = this;
if (offset === this.StartOffset) {
Expand All @@ -131,11 +207,64 @@ export default class PickerItem extends SuperComponent {
// 调整偏移量
const newOffset = range(offset + distance, -this.getCount() * pickItemHeight, 0);
const index = range(Math.round(-newOffset / pickItemHeight), 0, this.getCount() - 1);
this.setData({
offset: -index * pickItemHeight,
duration: ANIMATION_DURATION,
curIndex: index,
});

// 判断是否为快速惯性滚动
const isFastInertia = Math.abs(distance) > pickItemHeight * 3;

// 立即更新虚拟滚动视图(修复惯性滚动后空白问题,快速滚动时使用更大缓冲区)
if (this.data.enableVirtualScroll) {
this.updateVisibleOptions(-index * pickItemHeight, isFastInertia);
}

// 清除之前的动画更新定时器
if (this._animationTimer) {
clearInterval(this._animationTimer);
this._animationTimer = null;
}

// 在动画执行期间定期更新虚拟滚动视图(确保动画过程流畅)
if (this.data.enableVirtualScroll && Math.abs(distance) > 0) {
const startOffset = offset;
const endOffset = -index * pickItemHeight;
const startTime = Date.now();

this._animationTimer = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / ANIMATION_DURATION, 1);

// 使用缓动函数计算当前偏移量
const easeOutCubic = 1 - (1 - progress) ** 3;
const currentOffset = startOffset + (endOffset - startOffset) * easeOutCubic;

// 快速惯性滚动时使用更大的缓冲区
this.updateVisibleOptions(currentOffset, isFastInertia);

if (progress >= 1) {
clearInterval(this._animationTimer);
this._animationTimer = null;
}
}, 16); // 约60fps
}

this.setData(
{
offset: -index * pickItemHeight,
duration: ANIMATION_DURATION,
curIndex: index,
},
() => {
// 动画结束后清除定时器并最终更新虚拟滚动视图
if (this._animationTimer) {
clearInterval(this._animationTimer);
this._animationTimer = null;
}
if (this.data.enableVirtualScroll) {
// 动画结束后使用正常缓冲区(不再是快速滚动状态)
this.updateVisibleOptions(-index * pickItemHeight, false);
}
},
);

if (index === this._selectedIndex) return;
this.updateSelected(index, true);
},
Expand Down Expand Up @@ -167,24 +296,118 @@ export default class PickerItem extends SuperComponent {
const { options, value, pickerKeys, pickItemHeight, format, columnIndex } = this.data;

const formatOptions = this.formatOption(options, columnIndex, format);
const optionsCount = formatOptions.length;

const index = formatOptions.findIndex((item: PickerItemOption) => item[pickerKeys?.value] === value);
// 判断是否启用虚拟滚动
const enableVirtualScroll = optionsCount >= VIRTUAL_SCROLL_CONFIG.ENABLE_THRESHOLD;

// 大数据量优化:使用 Map 快速查找索引
let index: number = -1;
if (optionsCount > 500) {
// 构建临时 Map(只在查找时构建,不缓存)
const valueMap = new Map<any, number>(formatOptions.map((item, idx) => [item[pickerKeys?.value], idx]));
index = valueMap.get(value) ?? -1;
} else {
index = formatOptions.findIndex((item: PickerItemOption) => item[pickerKeys?.value] === value);
}
const selectedIndex = index > 0 ? index : 0;

this.setData(
{
formatOptions,
offset: -selectedIndex * pickItemHeight,
curIndex: selectedIndex,
},
() => {
this.updateSelected(selectedIndex, false);
},
);
const updateData: any = {
formatOptions,
offset: -selectedIndex * pickItemHeight,
curIndex: selectedIndex,
enableVirtualScroll,
totalHeight: optionsCount * pickItemHeight,
};

// 如果启用虚拟滚动,计算可见选项
if (enableVirtualScroll) {
const visibleRange = this.computeVirtualRange(-selectedIndex * pickItemHeight, optionsCount, pickItemHeight);
updateData.visibleOptions = formatOptions.slice(visibleRange.startIndex, visibleRange.endIndex);
updateData.virtualStartIndex = visibleRange.startIndex;
updateData.virtualOffsetY = visibleRange.startIndex * pickItemHeight;
} else {
// 不启用虚拟滚动时,visibleOptions 等于 formatOptions
updateData.visibleOptions = formatOptions;
updateData.virtualStartIndex = 0;
updateData.virtualOffsetY = 0;
}

this.setData(updateData, () => {
this.updateSelected(selectedIndex, false);
});
},

/**
* 计算虚拟滚动的可见范围
* @param offset 当前滚动偏移量
* @param totalCount 总选项数量
* @param itemHeight 单个选项高度
* @param isFastScroll 是否为快速滑动
*/
computeVirtualRange(offset: number, totalCount: number, itemHeight: number, isFastScroll = false) {
const scrollTop = Math.abs(offset);
const { VISIBLE_COUNT, BUFFER_COUNT, FAST_SCROLL_BUFFER } = VIRTUAL_SCROLL_CONFIG;

// 根据滑动速度动态调整缓冲区大小
const dynamicBuffer = isFastScroll ? FAST_SCROLL_BUFFER : BUFFER_COUNT;

// 根据滑动方向调整缓冲区分配
// 向上滑动(_scrollDirection = -1):增加顶部缓冲区
// 向下滑动(_scrollDirection = 1):增加底部缓冲区
const topBuffer = this._scrollDirection === -1 ? dynamicBuffer + 2 : dynamicBuffer;
const bottomBuffer = this._scrollDirection === 1 ? dynamicBuffer + 2 : dynamicBuffer;

// 计算当前可见区域的中心索引
const centerIndex = Math.floor(scrollTop / itemHeight);

// 计算起始索引(减去顶部缓冲区)
const startIndex = Math.max(0, centerIndex - topBuffer);
// 计算结束索引(加上可见数量和底部缓冲区)
const endIndex = Math.min(totalCount, centerIndex + VISIBLE_COUNT + bottomBuffer);

return { startIndex, endIndex };
},

/**
* 更新虚拟滚动的可见选项
* @param offset 当前滚动偏移量(可选,不传则使用 data.offset)
* @param isFastScroll 是否为快速滑动
*/
updateVisibleOptions(offset?: number, isFastScroll = false) {
const { formatOptions, pickItemHeight, enableVirtualScroll } = this.data;

if (!enableVirtualScroll) return;

const currentOffset = offset !== undefined ? offset : this.data.offset;
const visibleRange = this.computeVirtualRange(currentOffset, formatOptions.length, pickItemHeight, isFastScroll);

// 只有当可见范围发生变化时才更新
if (
visibleRange.startIndex !== this.data.virtualStartIndex ||
visibleRange.endIndex !== this.data.virtualStartIndex + this.data.visibleOptions.length
) {
this.setData({
visibleOptions: formatOptions.slice(visibleRange.startIndex, visibleRange.endIndex),
virtualStartIndex: visibleRange.startIndex,
virtualOffsetY: visibleRange.startIndex * pickItemHeight,
});
}
},

getCount() {
return this.data?.options?.length;
},

getCurrentSelected() {
const { offset, pickItemHeight, formatOptions, pickerKeys } = this.data;
const currentIndex = Math.max(0, Math.min(Math.round(-offset / pickItemHeight), this.getCount() - 1));

return {
index: currentIndex,
value: formatOptions[currentIndex]?.[pickerKeys?.value],
label: formatOptions[currentIndex]?.[pickerKeys?.label],
};
},
};
}
Loading
Loading