首页/新闻资讯/正文详情

小程序刻度尺组件(抖动问题待优化,需scrollend之后再做吸附处理)

发布时间:2026/9/25 18:16:25 来源:云帆数科 栏目:资讯中心
小程序刻度尺组件(抖动问题待优化,需scrollend之后再做吸附处理)
前言最近在开发一个小程序项目时需要实现一个身高测量功能调研了市面上常见的刻度尺组件后决定自己动手写一个。由于项目同时需要支持纵向身高和横向体重选择两种模式索性做了一个通用组件组件特性- 双向模式支持纵向和横向两种刻度尺- 密度可调通过 density 属性控制刻度线疏密- 高度可配每个刻度的高度/宽度可通过 itemHeight 灵活配置- 主题自定义支持自定义颜色、大小等样式属性- 交互友好滚动停止后自动吸附到刻度线核心实现结尾附完整代码核心原理利用 scroll-view 的 scrollTop/scrollLeft 控制滚动位置通过数学计算实现滚动位置与刻度值的双向转换。一、数据结构type ScaleItem { val: number // 刻度值 isBig: boolean // 是否大刻度显示数字和粗线 } //每个刻度线对应一个刻度值isBig 用于判断是否是大刻度。二、 刻度线生成buildScaleListV() { const { minValue, maxValue, step, density } this.properties const list: ScaleItem[] [] for (let i minValue; i maxValue; i density) { list.push({ val: i, isBig: i % step 0 }) } this.setData({ scaleList: list }) }关键点- density 控制循环步长实现刻度线疏密- isBig 基于刻度值判断不依赖 index三、滚动位置计算// 值 → 滚动位置 calcScrollTop(value: number) { const { minValue, density } this.properties const { itemHeight } this.data // snap 到最近的刻度线值 const validValue Math.round((value - minValue) / density) * density minValue const offset (validValue - minValue) * itemHeight this.setData({ scrollTop: Math.max(0, offset) }) } // 滚动位置 → 值 onScrollV(e: WechatMiniprogram.ScrollViewScroll) { const { minValue, maxValue } this.properties const { itemHeight } this.data const value Math.round(e.detail.scrollTop / itemHeight) minValue // ... }关键点- itemHeight 代表两个刻度值之间的物理距离- 滚动计算始终基于刻度值不考虑 density- calcScrollTop 会自动将值吸附到最近的刻度线四、布局结构!-- 纵向刻度尺 -- view classruler-vertical !-- 固定标记圆球指示线 -- view classscroll-marker-vertical.../view !-- 可滚动区域 -- scroll-view scroll-y bindscrollonScrollV view classspacer-top / view classscale-item-vertical styleheight: {{density * itemHeight}}px; wx:for{{scaleList}} !-- 大刻度/小刻度内容 -- /view view classspacer-bottom / /scroll-view /view关键点- 刻度线高度 density × itemHeight- 首尾 spacer 确保初始位置居中使用示例默认刻度尺!-- 纵向刻度尺身高测量 -- ruler directionvertical width{{120}} height{{450}} item-height{{13}} density{{3}} step{{10}} big-line-color#1890ff marker-color#1890ff min-value{{100}} max-value{{220}} default-value{{170}} bind:changeonRulerChange /完整代码ruler: 小程序刻度尺组件包含示例页面https://gitee.com/signup_xiuxiuxiu/ruler1. ruler.wxml!-- ruler 组件 - 纵向/横向刻度尺 -- !-- 纵向刻度尺 -- view classruler-vertical wx:if{{direction vertical}} stylewidth: {{width}}px; height: {{height}}px; background-color: {{bgColor}}; !-- 滚动标志圆球在左指示线在右 -- view classscroll-marker-vertical styleheight: {{ballSize}}px; top: {{ballTop}}px; view classmarker-ball stylewidth: {{ballSize}}px; height: {{ballSize}}px; background-color: {{markerColor}}; box-shadow: 0 1px 4px {{markerColor}}40; wx:if{{showBall}}/view view classmarker-line stylewidth: {{lineWidth}}px; height: 2px; background-color: {{markerColor}};/view /view !-- 刻度列表 -- scroll-view classruler-scroll-vertical scroll-y enhanced{{true}} show-scrollbar{{false}} bindscrollonScrollV scroll-top{{scrollTop}} view classspacer-top styleheight: {{height / 2}}px;/view view classscale-item-vertical {{item.isBig ? big : small}} styleheight: {{density * itemHeight}}px; wx:for{{scaleList}} wx:keyindex block wx:if{{item.isBig}} text classscale-text-vertical wx:if{{showScaleText}} stylefont-size: {{scaleFontSize}}px; color: {{scaleTextColor}}; {{item.val}}/text view classscale-line-big-vertical styleheight: {{bigLineHeight}}px; width: {{lineWidth}}px; background-color: {{bigLineColor}};/view /block block wx:else view classscale-line-vertical styleheight: {{smallLineHeight}}px; width: {{smallLineWidth}}px; background-color: {{smallLineColor}};/view /block /view view classspacer-bottom styleheight: {{height / 2}}px;/view /scroll-view /view !-- 横向刻度尺 -- view classruler-horizontal wx:if{{direction horizontal}} stylewidth: {{width}}px; height: {{height}}px; background-color: {{bgColor}}; !-- 滚动标志指示线在上圆球在下 -- view classscroll-marker-horizontal stylewidth: {{ballSize}}px;left: {{ballLeft}}px; view classmarker-line-h stylewidth: 2px; height: {{lineWidth}}px; background-color: {{markerColor}};/view view classmarker-ball-h stylewidth: {{ballSize}}px; height: {{ballSize}}px; background-color: {{markerColor}}; box-shadow: 0 1px 4px {{markerColor}}40; wx:if{{showBall}}/view /view !-- 刻度列表 -- scroll-view classruler-scroll-horizontal scroll-x enhanced{{true}} show-scrollbar{{false}} bindscrollonScrollH scroll-left{{scrollLeft}} view classscroll-content-horizontal view classspacer-left stylewidth: {{width / 2}}px;/view view classscale-item-horizontal {{item.isBig ? big : small}} stylewidth: {{density * itemHeight}}px; wx:for{{scaleList}} wx:keyindex block wx:if{{item.isBig}} view classscale-line-big-horizontal stylewidth: {{bigLineHeight}}px; height: {{lineWidth}}px; background-color: {{bigLineColor}};/view text classscale-text-horizontal wx:if{{showScaleText}} stylefont-size: {{scaleFontSize}}px; color: {{scaleTextColor}};{{item.val}}/text /block block wx:else view classscale-line-horizontal stylewidth: {{smallLineHeight}}px; height: {{smallLineWidth}}px; background-color: {{smallLineColor}};/view /block /view view classspacer-right stylewidth: {{width / 2}}px;/view /view /scroll-view /view2. ruler.scss/* 纵向刻度尺 */ .ruler-vertical { position: relative; overflow: hidden; } .scroll-marker-vertical { position: absolute; right: 0; z-index: 10; display: flex; align-items: center; transition: top 0.1s ease-out; } .marker-ball { border-radius: 50%; } .ruler-scroll-vertical { width: 100%; height: 100%; } .scale-item-vertical { display: flex; align-items: flex-start; box-sizing: border-box; .big { justify-content: flex-end; } .small { justify-content: flex-end; } } .scale-text-vertical { transform: translateY(-10px); text-align: right; box-sizing: border-box; padding-right: 15px; } .scale-line-big-vertical { height: 2px; } .scale-line-vertical { height: 1px; } /* 横向刻度尺 */ .ruler-horizontal { position: relative; overflow: hidden; } .scroll-marker-horizontal { position: absolute; top: 0; z-index: 10; display: flex; flex-direction: column; align-items: center; transition: left 0.1s ease-out; } .marker-ball-h { border-radius: 50%; } .ruler-scroll-horizontal { width: 100%; height: 100%; } .scroll-content-horizontal { display: flex; flex-direction: row; align-items: flex-start; height: 100%; width: max-content; } .spacer-left, .spacer-right { flex-shrink: 0; } .scale-item-horizontal { display: flex; flex-direction: column; flex-shrink: 0; box-sizing: border-box; .big { justify-content: flex-end; } .small { justify-content: flex-start; } } .scale-text-horizontal { text-align: center; box-sizing: border-box; padding-top: 15px; transform: translateX(-10px); }3. ruler.ts/** * ruler 组件 - 纵向/横向刻度尺 * * 实现思路 * 1. 通过 direction 切换纵向/横向 * 2. 纵向scroll-y 滚动scrollTop 控制 * 3. 横向scroll-x 滚动scrollLeft 控制 * 4. 滚动标志圆点指示线在固定位置 */ type ScaleItem { val: number // 刻度值 isBig: boolean // 是否大刻度 } Component({ properties: { // 方向配置 direction: { type: String, value: vertical // vertical | horizontal }, // 尺寸配置 width: { type: Number, value: 90 }, height: { type: Number, value: 312 }, itemHeight: { type: Number, value: 13 }, density: { type: Number, value: 1 }, // 刻度样式配置 step: { type: Number, value: 10 }, showScaleText: { type: Boolean, value: true }, scaleFontSize: { type: Number, value: 14 }, scaleTextColor: { type: String, value: #000 }, bigLineHeight: { type: Number, value: 2 }, smallLineHeight: { type: Number, value: 1 }, smallLineWidth: { type: Number, value: 20 }, lineWidth: { type: Number, value: 40 }, ballSize: { type: Number, value: 10 }, // 颜色配置 bigLineColor: { type: String, value: #FD6321 }, smallLineColor: { type: String, value: rgba(253, 99, 33, 0.3) }, markerColor: { type: String, value: #FD6321 }, bgColor: { type: String, value: rgba(253, 99, 33, 0.06) }, showBall: { type: Boolean, value: true }, // 刻度尺范围配置 minValue: { type: Number, value: 100 }, maxValue: { type: Number, value: 220 }, defaultValue: { type: Number, value: 170 } }, data: { scaleList: [] as ScaleItem[], // 纵向 scrollTop: 0, ballTop: 0, containerHeight: 0, // 横向 scrollLeft: 0, ballLeft: 0, containerWidth: 0, // 当前值 currentValue: 170, itemHeight: 13, debounceTimerV: null as number | null, debounceTimerH: null as number | null, }, lifetimes: { attached() { this.initRuler() } }, observers: { defaultValue: function (val: number) { if (val ! this.data.currentValue) { this.setData({ currentValue: val }) if (this.properties.direction horizontal) { this.calcScrollLeft(val) } else { this.calcScrollTop(val) } } } }, methods: { initRuler() { const { defaultValue, itemHeight, direction } this.properties as any this.setData({ itemHeight }) if (direction horizontal) { this.buildScaleListH() this.updateContainerWidth(() { this.calcScrollLeft(defaultValue) }) } else { this.buildScaleListV() this.updateContainerHeight(() { this.calcScrollTop(defaultValue) }) } }, // 纵向 buildScaleListV() { const { minValue, maxValue, step, density } this.properties as any const list: ScaleItem[] [] for (let i minValue; i maxValue; i density) { list.push({ val: i, isBig: i % step 0 }) } this.setData({ scaleList: list }) }, calcScrollTop(value: number) { const { minValue, density } this.properties as any const { itemHeight } this.data // snap 到最近的刻度线值 const validValue Math.round((value - minValue) / density) * density minValue const offset (validValue - minValue) * itemHeight this.setData({ scrollTop: Math.max(0, offset), currentValue: validValue }) }, updateContainerHeight(callback?: () void) { const { ballSize } this.properties as any const query wx.createSelectorQuery().in(this) query.select(.ruler-vertical).boundingClientRect((rect) { if (rect) { const containerHeight rect.height const ballTop containerHeight / 2 - ballSize / 2 this.setData({ containerHeight, ballTop }) if (callback) callback() } }).exec() }, onScrollV(e: WechatMiniprogram.ScrollViewScroll) { const { minValue, maxValue } this.properties as any const { itemHeight, debounceTimerV } this.data const value Math.round(e.detail.scrollTop / itemHeight) minValue const clampedValue Math.max(minValue, Math.min(maxValue, value)) this.setData({ currentValue: clampedValue }) this.triggerEvent(change, { value: clampedValue }) if (debounceTimerV) { clearTimeout(debounceTimerV) } const timer setTimeout(() { this.calcScrollTop(clampedValue) }, 100) this.setData({ debounceTimerV: timer }) }, // 横向 buildScaleListH() { const { minValue, maxValue, step, density } this.properties as any const list: ScaleItem[] [] for (let i minValue; i maxValue; i density) { list.push({ val: i, isBig: i % step 0 }) } this.setData({ scaleList: list }) }, calcScrollLeft(value: number) { const { minValue, density } this.properties as any const { itemHeight } this.data // snap 到最近的刻度线值 const validValue Math.round((value - minValue) / density) * density minValue const offset (validValue - minValue) * itemHeight this.setData({ scrollLeft: Math.max(0, offset), currentValue: validValue }) }, updateContainerWidth(callback?: () void) { const { ballSize } this.properties as any const query wx.createSelectorQuery().in(this) query.select(.ruler-horizontal).boundingClientRect((rect) { if (rect) { const containerWidth rect.width const ballLeft containerWidth / 2 - ballSize / 2 this.setData({ containerWidth, ballLeft }) if (callback) callback() } }).exec() }, onScrollH(e: WechatMiniprogram.ScrollViewScroll) { const { minValue, maxValue } this.properties as any const { itemHeight, debounceTimerH } this.data const value Math.round(e.detail.scrollLeft / itemHeight) minValue const clampedValue Math.max(minValue, Math.min(maxValue, value)) this.setData({ currentValue: clampedValue }) this.triggerEvent(change, { value: clampedValue }) if (debounceTimerH) { clearTimeout(debounceTimerH) } const timer setTimeout(() { this.calcScrollLeft(clampedValue) }, 100) this.setData({ debounceTimerH: timer }) }, } })4. ruler.json{ component: true, usingComponents: {} }配置介绍仓库内的README.md)## 属性说明 ### 方向配置 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | direction | String | vertical | 刻度尺方向vertical 纵向horizontal 横向 | ### 尺寸配置 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | width | Number | 90 | 刻度尺宽度px | | height | Number | 312 | 刻度尺高度px | | itemHeight | Number | 13 | 两个刻度值之间的物理距离px每条刻度线的实际高度 density × itemHeight | ### 刻度样式 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | density | Number | 1 | 刻度线密度每隔 density 个刻度值显示一条刻度线 | | step | Number | 10 | 大刻度间隔每隔多少显示数字和大刻度线 | | showScaleText | Boolean | true | 是否显示刻度数字 | | scaleFontSize | Number | 14 | 刻度数字字号px | | scaleTextColor | String | #000 | 刻度数字颜色 | | bigLineHeight | Number | 2 | 大刻度线高度px | | smallLineHeight | Number | 1 | 小刻度线高度px | | smallLineWidth | Number | 20 | 小刻度线宽度px | | lineWidth | Number | 40 | 刻度线总宽度px | ### 颜色配置 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | bigLineColor | String | #FD6321 | 大刻度线颜色 | | smallLineColor | String | rgba(253,99,33,0.3) | 小刻度线颜色 | | markerColor | String | #FD6321 | 圆球和指示线颜色 | | bgColor | String | rgba(253,99,33,0.06) | 刻度尺背景色 | ### 功能配置 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | ballSize | Number | 10 | 圆球尺寸px | | showBall | Boolean | true | 是否显示圆球 | | minValue | Number | 100 | 刻度尺最小值 | | maxValue | Number | 220 | 刻度尺最大值 | | defaultValue | Number | 170 | 默认值 | ## 事件 ### change 滚动停止后触发返回当前选中值。 xml ruler bind:changeonRulerChange / js onRulerChange(e) { const { value } e.detail console.log(选中值:, value) } ## 使用示例 ### 纵向刻度尺身高测量 xml ruler directionvertical width{{120}} height{{450}} item-height{{13}} density{{3}} step{{10}} show-scale-text{{true}} scale-font-size{{14}} scale-text-color#000 big-line-height{{2}} small-line-height{{1}} small-line-width{{20}} line-width{{40}} ball-size{{10}} big-line-color#1890ff small-line-colorrgba(24, 144, 255, 0.3) marker-color#1890ff bg-colorrgba(24, 144, 255, 0.06) show-ball{{true}} min-value{{100}} max-value{{220}} default-value{{170}} bind:changeonRulerChange / ## 注意事项 1. **itemHeight**代表两个刻度值之间的物理距离px刻度线的实际高度 density × itemHeight 2. **density**刻度线密度1每个刻度值都有刻度线2每隔一个刻度值有刻度线以此类推 3. **滚动范围**刻度范围从 minValue 到 maxValue首尾有半个容器宽/高度的空白区域 4. **默认值**组件初始化时会自动滚动到 defaultValue 指定的位置会自动对齐到刻度线 5. **响应式**修改 defaultValue 会触发组件重新定位到指定值总结这个组件的实现思路比较简单核心就是利用 scroll-view 的 scrollTop/scrollLeft控制滚动位置通过计算实现值与滚动位置的相互转换。density属性的引入让刻度线的疏密变得灵活可调希望能给有类似需求的同学一些参考因为考虑不同项目色调不同尺子的背景颜色刻度线的颜色都可以配置有需要的可以参考实现哈有更好的优化建议也欢迎各位大佬指出共同进步๑ᴗ๑

相关推荐

微信小程序+SSM+MySQL设备故障报修系统毕业设计全流程解析
微信小程序+SSM+MySQL设备故障报修系统毕业设计全流程解析

简介:设备故障报修小程序毕业设计资源包,基于微信小程序SSMMySql开发,面向计算机相关专业毕业生及需要快速搭建完整前后端项目的开发者,解决高校实验室设备报修、维修管理、经验分享等日常运维痛点。系统涵盖管理员、用户、维修员… · 2026/9/25 18:16:19

WPF自定义控件实战:用ProgressBar模板打造垂直温度计
WPF自定义控件实战:用ProgressBar模板打造垂直温度计

简介:这份资源面向具备一定WPF基础的开发者,聚焦如何把默认水平的ProgressBar改造成垂直温度计样式的自定义控件,解决常规进度条难以满足仪表类UI需求的问题。内容围绕Orientation属性设置、ControlTemplate模板重写、Path指针与ScaleTransfo… · 2026/9/25 18:16:19

NVM的安装、使用(Linux系统)
NVM的安装、使用(Linux系统)

摘要 这篇文章主要讲解 Linux 安装 NVM 并用它管理 多个版本的Node.js 的完整流程:配置环境变量后,通过 nvm install 下载多个 Node 版本,用 nvm use 按项目需求切换,解决不同前端项目对 Node 版本要求不同的问题。最后总结三者关… · 2026/9/25 18:16:19

告别传统微调!用 Context Engineering 配 TaoToken 打造强大 AI Agent
告别传统微调!用 Context Engineering 配 TaoToken 打造强大 AI Agent

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 18:51:01

【深度学习精通】第22章 | 大语言模型 - 从GPT-3到GPT-4与LLaMA:用TaoToken统一Key跑通三组推理配置
【深度学习精通】第22章 | 大语言模型 - 从GPT-3到GPT-4与LLaMA:用TaoToken统一Key跑通三组推理配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 18:50:55

AGIOne 产品架构解析:模型服务层、算力运行层与公共治理域
AGIOne 产品架构解析:模型服务层、算力运行层与公共治理域

摘要:本文从模型服务层、算力运行层、公共治理域和应用延展层出发,梳理 AGIOne 从算力接入、模型部署到服务发布、授权调用与计量运营的完整运行链路。AGIOne 是企业级 AI 运行时基础设施平台,其架构由模型服务层、算力运行层和贯穿两层的公共… · 2026/9/25 18:50:55

WeiXinMPSDK 微信支付 V3 的 Apis 目录代码组织规则:命名空间与类名约定深度解析
WeiXinMPSDK 微信支付 V3 的 Apis 目录代码组织规则:命名空间与类名约定深度解析

后端即时通讯金融科技 【免费下载链接】WeiXinMPSDK 微信全平台 .NET SDK, Senparc.Weixin for C#,支持 .NET Framework 及 .NET Core、.NET 10.0。已支持微信公众号、小程序、小游戏、微信支付、企业微信/企业号、开放平台、JSSDK、微信周边等全平台。 … · 2026/9/25 18:50:36

Ghidra MCP 无头服务器部署指南:Docker容器化与CI/CD批量逆向分析
Ghidra MCP 无头服务器部署指南:Docker容器化与CI/CD批量逆向分析

Ghidra MCP 无头服务器部署指南:Docker容器化与CI/CD批量逆向分析 【免费下载链接】ghidra-mcp Ghidra MCP Server — 200 MCP tools for AI-powered reverse engineering. GUI plugin headless server, lazy tool loading, convention enforcement, batch operati… · 2026/9/25 18:50:30

基于 Simulink 的永磁同步电机(PMSM)模型预测脉宽调制(MP-PWM)控制仿真
基于 Simulink 的永磁同步电机(PMSM)模型预测脉宽调制(MP-PWM)控制仿真

目录 一、 核心原理:从“遍历开关”到“连续寻优” 1. MP-PWM 的控制架构 2. 代价函数与权重设计 3. 多物理量约束处理 二、 Simulink 建模步骤(手把手 5 步法) Step 1:搭建 PMSM 本体与逆变器模型 Step 2:配置… · 2026/9/25 18:50:24

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码