From a955b14c3b173cc0c65aba36f32913d92b503d3b Mon Sep 17 00:00:00 2001 From: jarmywang Date: Mon, 15 Dec 2025 13:19:25 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(picker):=20=E4=BC=98=E5=8C=96=E6=80=A7?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E9=81=BF=E5=85=8D=E6=8E=89=E5=B8=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/picker-item/picker-item.ts | 117 ++++++++++-------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/packages/components/picker-item/picker-item.ts b/packages/components/picker-item/picker-item.ts index 275841d83..d45d90e77 100644 --- a/packages/components/picker-item/picker-item.ts +++ b/packages/components/picker-item/picker-item.ts @@ -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`时,执行惯性滚动 @@ -130,7 +131,7 @@ export default class PickerItem extends SuperComponent { onTouchMove(event) { const { StartY, StartOffset } = this; - const { itemHeight, enableVirtualScroll } = this.data; + const { itemHeight, enableVirtualScroll, formatOptions } = this.data; const currentTime = Date.now(); // 偏移增量 @@ -161,11 +162,26 @@ export default class PickerItem extends SuperComponent { this._moveTimer = null; } - this.setData({ offset: newOffset }); - - // 虚拟滚动:更新可见范围(快速滑动时使用更大的缓冲区) + // 性能优化:合并 setData 调用,减少逻辑层到渲染层的通信次数 if (enableVirtualScroll) { - this.updateVisibleOptions(newOffset, isFastScroll); + const visibleRange = this.computeVirtualRange(newOffset, formatOptions.length, itemHeight, isFastScroll); + // 只有当可见范围发生变化时才更新虚拟滚动数据 + if ( + visibleRange.startIndex !== this.data.virtualStartIndex || + visibleRange.endIndex !== this.data.virtualStartIndex + this.data.visibleOptions.length + ) { + this.setData({ + offset: newOffset, + visibleOptions: formatOptions.slice(visibleRange.startIndex, visibleRange.endIndex), + virtualStartIndex: visibleRange.startIndex, + virtualOffsetY: visibleRange.startIndex * itemHeight, + }); + } else { + // 可见范围未变化,只更新 offset + this.setData({ offset: newOffset }); + } + } else { + this.setData({ offset: newOffset }); } this._moveTimer = setTimeout(() => { @@ -184,7 +200,7 @@ export default class PickerItem extends SuperComponent { this._moveTimer = null; } - const { offset, itemHeight } = this.data; + const { offset, itemHeight, enableVirtualScroll, formatOptions } = this.data; const { startTime } = this; if (offset === this.StartOffset) { return; @@ -201,14 +217,20 @@ 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) { @@ -216,48 +238,43 @@ export default class PickerItem extends SuperComponent { 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); From 747fad09ff3b79e656eff2da1df257d0bca6987b Mon Sep 17 00:00:00 2001 From: jarmywang Date: Mon, 15 Dec 2025 14:16:00 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(picker):=20=E5=A2=9E=E5=8A=A0=E5=A4=A7?= =?UTF-8?q?=E9=87=8F=E6=95=B0=E6=8D=AE=E6=B5=8B=E8=AF=95=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/picker/_example/area/index.js | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/components/picker/_example/area/index.js b/packages/components/picker/_example/area/index.js index 639b23563..e41ad8302 100644 --- a/packages/components/picker/_example/area/index.js +++ b/packages/components/picker/_example/area/index.js @@ -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] })); From f4985941fb313c1248cd594bad3ef2b5c5fc2577 Mon Sep 17 00:00:00 2001 From: Boomkaa <173308582@qq.com> Date: Tue, 16 Dec 2025 11:27:31 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E5=87=8F=E5=B0=91touchMove=E6=9C=9F?= =?UTF-8?q?=E9=97=B4=E7=9A=84setData=E6=95=B0=E6=8D=AE=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/picker-item/picker-item.ts | 62 ++----------------- 1 file changed, 5 insertions(+), 57 deletions(-) diff --git a/packages/components/picker-item/picker-item.ts b/packages/components/picker-item/picker-item.ts index d45d90e77..8ba73ea86 100644 --- a/packages/components/picker-item/picker-item.ts +++ b/packages/components/picker-item/picker-item.ts @@ -131,67 +131,15 @@ export default class PickerItem extends SuperComponent { onTouchMove(event) { const { StartY, StartOffset } = this; - const { itemHeight, enableVirtualScroll, formatOptions } = 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; - } - - // 性能优化:合并 setData 调用,减少逻辑层到渲染层的通信次数 - if (enableVirtualScroll) { - const visibleRange = this.computeVirtualRange(newOffset, formatOptions.length, itemHeight, isFastScroll); - // 只有当可见范围发生变化时才更新虚拟滚动数据 - if ( - visibleRange.startIndex !== this.data.virtualStartIndex || - visibleRange.endIndex !== this.data.virtualStartIndex + this.data.visibleOptions.length - ) { - this.setData({ - offset: newOffset, - visibleOptions: formatOptions.slice(visibleRange.startIndex, visibleRange.endIndex), - virtualStartIndex: visibleRange.startIndex, - virtualOffsetY: visibleRange.startIndex * itemHeight, - }); - } else { - // 可见范围未变化,只更新 offset - this.setData({ offset: newOffset }); - } - } else { - this.setData({ offset: newOffset }); - } - - this._moveTimer = setTimeout(() => { - this._moveTimer = null; - }, VIRTUAL_SCROLL_CONFIG.THROTTLE_TIME); - } + const newOffset = range(StartOffset + deltaY, -(this.getCount() - 1) * itemHeight, 0); - // 记录当前状态 - this._lastOffset = newOffset; - this._lastMoveTime = currentTime; + // touchMove 期间只更新 offset,不更新虚拟滚动数据 + // 虚拟滚动数据在 touchEnd 时统一更新,避免频繁 setData 导致掉帧 + this.setData({ offset: newOffset }); }, onTouchEnd(event) { From 2f255d7f51269a90ac91e6be7dea4a44d88b69a9 Mon Sep 17 00:00:00 2001 From: jarmywang Date: Wed, 24 Dec 2025 23:11:02 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(picker):=20=E5=88=A0=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E5=8F=98=E9=87=8F=E5=92=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/picker-item/picker-item.ts | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/components/picker-item/picker-item.ts b/packages/components/picker-item/picker-item.ts index 8ba73ea86..ab6cfbddc 100644 --- a/packages/components/picker-item/picker-item.ts +++ b/packages/components/picker-item/picker-item.ts @@ -88,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; @@ -143,11 +135,6 @@ export default class PickerItem extends SuperComponent { }, onTouchEnd(event) { - if (this._moveTimer) { - clearTimeout(this._moveTimer); - this._moveTimer = null; - } - const { offset, itemHeight, enableVirtualScroll, formatOptions } = this.data; const { startTime } = this; if (offset === this.StartOffset) { @@ -314,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 }; }, From de2c9499a5a9206b2ddf4402abcccb691b0b5e74 Mon Sep 17 00:00:00 2001 From: jarmywang Date: Wed, 24 Dec 2025 23:48:05 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(picker):=20=E8=A7=A3=E5=86=B3=E5=B9=B3?= =?UTF-8?q?=E9=93=BA=E6=A8=A1=E5=BC=8F=E6=9C=AA=E5=93=8D=E5=BA=94value?= =?UTF-8?q?=E5=8F=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/picker/picker.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/components/picker/picker.ts b/packages/components/picker/picker.ts index 158807d11..f4ddb9dd3 100644 --- a/packages/components/picker/picker.ts +++ b/packages/components/picker/picker.ts @@ -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(); }