Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
29 changes: 29 additions & 0 deletions packages/components/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,32 @@ export const nextTick = () => {
});
});
};

export const convertUnit = (value: number | string | null | undefined): number => {
if (value == null) return 0;
if (typeof value === 'number') return rpx2px(value);

const match = value.trim().match(/^([-+]?[\d.]+)(\w*)$/);
if (!match) return 0;

const [, numStr, unit] = match;
const num = parseFloat(numStr);

if (!unit) {
return num;
}

switch (unit.toLowerCase()) {
case 'px':
return num;
case 'rpx':
return rpx2px(num);
case 'vw':
return (num * systemInfo.windowWidth) / 100;
case 'vh':
return (num * systemInfo.windowHeight) / 100;
default:
console.warn(`[convertUnit] 不支持的单位: ${unit}`);
return 0;
}
};
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