Skip to content
Merged
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
168 changes: 57 additions & 111 deletions packages/components/picker-item/picker-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { PickerItemOption } from './type';
const { prefix } = config;
const name = `${prefix}-picker-item`;

// 动画持续时间
const ANIMATION_DURATION = 1000;
// 动画持续时间(优化:根据滑动距离动态计算,基础时长降低)
const ANIMATION_DURATION_BASE = 300; // 基础动画时长
const ANIMATION_DURATION_MAX = 600; // 最大动画时长
// 和上一次move事件间隔小于INERTIA_TIME
const INERTIA_TIME = 300;
// 且距离大于`MOMENTUM_DISTANCE`时,执行惯性滚动
Expand Down Expand Up @@ -87,18 +88,10 @@ export default class PickerItem extends SuperComponent {
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 @@ -130,61 +123,19 @@ export default class PickerItem extends SuperComponent {

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

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

// 计算滑动速度和方向
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;
}
const newOffset = range(StartOffset + deltaY, -(this.getCount() - 1) * itemHeight, 0);

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;
// touchMove 期间只更新 offset,不更新虚拟滚动数据
// 虚拟滚动数据在 touchEnd 时统一更新,避免频繁 setData 导致掉帧
this.setData({ offset: newOffset });
},

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

const { offset, itemHeight } = this.data;
const { offset, itemHeight, enableVirtualScroll, formatOptions } = this.data;
const { startTime } = this;
if (offset === this.StartOffset) {
return;
Expand All @@ -201,63 +152,64 @@ export default class PickerItem extends SuperComponent {
// 调整偏移量
const newOffset = range(offset + distance, -this.getCount() * itemHeight, 0);
const index = range(Math.round(-newOffset / itemHeight), 0, this.getCount() - 1);
const finalOffset = -index * itemHeight;

// 动态计算动画时长:根据滑动距离调整,距离越大时长越长,但有上限
const scrollDistance = Math.abs(finalOffset - offset);
const scrollItems = scrollDistance / itemHeight;
const animationDuration = Math.min(
ANIMATION_DURATION_MAX,
ANIMATION_DURATION_BASE + scrollItems * 30, // 每滑动一个选项增加30ms
);

// 判断是否为快速惯性滚动
// 判断是否为快速惯性滚动(用于决定缓冲区大小)
const isFastInertia = Math.abs(distance) > itemHeight * 3;

// 立即更新虚拟滚动视图(修复惯性滚动后空白问题,快速滚动时使用更大缓冲区)
if (this.data.enableVirtualScroll) {
this.updateVisibleOptions(-index * itemHeight, isFastInertia);
}
// 根据是否快速惯性滚动选择缓冲区大小
const bufferCount = isFastInertia ? VIRTUAL_SCROLL_CONFIG.FAST_SCROLL_BUFFER : VIRTUAL_SCROLL_CONFIG.BUFFER_COUNT;

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

// 在动画执行期间定期更新虚拟滚动视图(确保动画过程流畅)
if (this.data.enableVirtualScroll && Math.abs(distance) > 0) {
const startOffset = offset;
const endOffset = -index * itemHeight;
const startTime = Date.now();
// 性能优化:合并 setData 调用,一次性更新所有状态
const updateData: any = {
offset: finalOffset,
duration: animationDuration,
curIndex: index,
};

this._animationTimer = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / ANIMATION_DURATION, 1);
// 虚拟滚动:预先计算覆盖动画全程的可见范围,避免动画期间频繁更新
if (enableVirtualScroll) {
// 计算当前位置和目标位置的索引范围
const currentIndex = Math.floor(Math.abs(offset) / itemHeight);
const targetIndex = index;

// 使用缓动函数计算当前偏移量
const easeOutCubic = 1 - (1 - progress) ** 3;
const currentOffset = startOffset + (endOffset - startOffset) * easeOutCubic;
// 计算覆盖动画全程的可见范围(从当前位置到目标位置)
const minIndex = Math.min(currentIndex, targetIndex);
const maxIndex = Math.max(currentIndex, targetIndex);

// 快速惯性滚动时使用更大的缓冲区
this.updateVisibleOptions(currentOffset, isFastInertia);
// 使用缓冲区扩展范围,确保动画过程中不会出现空白
const animationStartIndex = Math.max(0, minIndex - bufferCount);
const animationEndIndex = Math.min(formatOptions.length, maxIndex + this.data.visibleItemCount + bufferCount);

if (progress >= 1) {
clearInterval(this._animationTimer);
this._animationTimer = null;
}
}, 16); // 约60fps
updateData.visibleOptions = formatOptions.slice(animationStartIndex, animationEndIndex);
updateData.virtualStartIndex = animationStartIndex;
updateData.virtualOffsetY = animationStartIndex * itemHeight;
}

this.setData(
{
offset: -index * itemHeight,
duration: ANIMATION_DURATION,
curIndex: index,
},
() => {
// 动画结束后清除定时器并最终更新虚拟滚动视图
if (this._animationTimer) {
clearInterval(this._animationTimer);
this._animationTimer = null;
}
if (this.data.enableVirtualScroll) {
// 动画结束后使用正常缓冲区(不再是快速滚动状态)
this.updateVisibleOptions(-index * itemHeight, false);
}
},
);
this.setData(updateData, () => {
// 动画结束后,精确更新虚拟滚动视图到最终位置
if (enableVirtualScroll) {
const visibleRange = this.computeVirtualRange(finalOffset, formatOptions.length, itemHeight, false);
this.setData({
visibleOptions: formatOptions.slice(visibleRange.startIndex, visibleRange.endIndex),
virtualStartIndex: visibleRange.startIndex,
virtualOffsetY: visibleRange.startIndex * itemHeight,
});
}
});

if (index === this._selectedIndex) return;
this.updateSelected(index, true);
Expand Down Expand Up @@ -349,21 +301,15 @@ export default class PickerItem extends SuperComponent {
const { visibleItemCount } = this.data;

// 根据滑动速度动态调整缓冲区大小
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 bufferCount = isFastScroll ? FAST_SCROLL_BUFFER : BUFFER_COUNT;

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

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

return { startIndex, endIndex };
},
Expand Down
55 changes: 55 additions & 0 deletions packages/components/picker/_example/area/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,61 @@ const areaList = {
},
};

// 使用这份数据可以模拟大量数据场景
// // 生成广东省1000个城市
// const generateCities = () => {
// const cities = {
// 110100: '北京市',
// };
//
// // 生成广东省1000个城市
// for (let i = 1; i <= 1000; i++) {
// const cityCode = 440000 + i * 100;
// cities[cityCode] = `广东城市${i}`;
// }
//
// return cities;
// };
//
// // 生成广州市10000个地区
// const generateCounties = () => {
// const counties = {
// 110101: '东城区',
// 110102: '西城区',
// 110105: '朝阳区',
// 110106: '丰台区',
// 110107: '石景山区',
// 110108: '海淀区',
// 110109: '门头沟区',
// 110111: '房山区',
// 110112: '通州区',
// 110113: '顺义区',
// 110114: '昌平区',
// 110115: '大兴区',
// 110116: '怀柔区',
// 110117: '平谷区',
// 110118: '密云区',
// 110119: '延庆区',
// };
//
// // 生成广州市(440100)10000个地区
// for (let i = 1; i <= 10000; i++) {
// const countyCode = 44010000 + i;
// counties[countyCode] = `广州地区${i}`;
// }
//
// return counties;
// };
//
// const areaList = {
// provinces: {
// 110000: '北京市',
// 440000: '广东省',
// },
// cities: generateCities(),
// counties: generateCounties(),
// };

const getOptions = (obj, filter) => {
const res = Object.keys(obj).map((key) => ({ value: key, label: obj[key] }));

Expand Down
6 changes: 4 additions & 2 deletions packages/components/picker/picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ export default class Picker extends SuperComponent {

observers = {
'value, visible'(value, visible) {
// 只在打开弹窗或value变化时更新,关闭时不更新避免滚动
if (visible) {
const { usePopup } = this.properties;
// 平铺模式(usePopup=false):始终响应 value 变化
// 弹窗模式(usePopup=true):只在打开弹窗时更新,关闭时不更新避免回弹
if (!usePopup || visible) {
this.updateChildren();
this.updateIndicatorPosition();
}
Expand Down
Loading