鸿蒙开发日记:撸了个单位换算器,顺便把温度换算搞明白了
鸿蒙开发日记撸了个单位换算器顺便把温度换算搞明白了缘起上周末做饭菜谱上写的是盎司我家厨房秤只显示克。掏出手机现找换算工具心想这玩意儿我也能做啊正好最近在学鸿蒙开发就当练手项目了。说干就干一个周末下来还真让我给撸出来了。第一天想清楚要做什么功能定位既然是练手功能不用太复杂但也得有实用性。我决定支持四大类长度mm、cm、m、km、英寸、英尺重量mg、g、kg、吨、磅、盎司温度摄氏、华氏、开尔文面积mm²、cm²、m²、km²、公顷、英亩这些基本覆盖日常需求了。UI 怎么设计参考了几个主流换算App发现大家都用类似的布局顶部类别切换Tab输入区域选单位 输数值输出区域选单位 看结果交换按钮简洁明了我也这么干。第二天数据结构磨刀不误砍柴工单位怎么定义每个单位需要两个属性名字和换算系数。interfaceUnitInfo{label:string// 显示名称toBase:number// 换算到基准单位的系数}系数是啥意思比如长度我选米作为基准1厘米 0.01米所以toBase 0.011千米 1000米所以toBase 10001英寸 0.0254米对就是这么精确这样换算起来就简单了输入值 × toBase 基准单位值 基准单位值 ÷ 目标toBase 结果类别怎么定义interfaceCategory{name:stringicon:stringunits:UnitInfo[]convertFn?:(val,fromIdx,toIdx)number// 特殊换算函数}为什么需要convertFn因为温度不一样摄氏、华氏、开尔文之间不是简单的乘除关系需要特殊公式。这个后面细说。完整数据constCATEGORIES[{name:长度,icon:,units:[{label:毫米 (mm),toBase:0.001},{label:厘米 (cm),toBase:0.01},{label:米 (m),toBase:1},{label:千米 (km),toBase:1000},{label:英寸 (in),toBase:0.0254},{label:英尺 (ft),toBase:0.3048},]},// ... 其他类别]第三天换算算法基准单位法普通单位的换算思路很简单先把输入转成基准单位再转成目标单位。convert():void{constnumparseFloat(this.inputValue)constcatCATEGORIES[this.activeCategory]// 输入值 → 基准单位constbasenum*cat.units[this.fromIdx].toBase// 基准单位 → 目标单位constresultbase/cat.units[this.toIdx].toBasethis.resultValuethis.formatResult(result)}举个例子5千米换算成米输入5转基准5 × 1000 5000米转目标5000 ÷ 1 5000米完美第一个坑温度不是这么算的我一开始把温度也用系数法定义了结果测试时发现100°C 换算成华氏度结果是 100而不是 212。问题在哪温度换算不是线性的摄氏度和华氏度的关系是°C → °FC × 9/5 32°F → °C(F - 32) × 5/9开尔文也是一样°C → KC 273.15K → °CK - 273.15解决方案写个特殊函数先转成摄氏度再转成目标单位convertFn:(val,fromIdx,toIdx){// 第一步转摄氏度letcelsiusif(fromIdx0)celsiusval// 摄氏度elseif(fromIdx1)celsius(val-32)*5/9// 华氏度elsecelsiusval-273.15// 开尔文// 第二步转目标if(toIdx0)returncelsiusif(toIdx1)returncelsius*9/532returncelsius273.15}第四天UI 布局卡片式设计整体结构Column ├── 标题 ├── 类别TabRow ForEach ├── 换算卡片Column │ ├── 输入区 │ ├── 交换按钮 │ └── 输出区 └── 底部提示类别Tab用ForEach遍历类别数组每个Tab是一个ColumnRow(){ForEach(CATEGORIES,(cat,idx){Column(){Text(cat.icon).fontSize(22)Text(cat.name).fontSize(13)}.backgroundColor(this.activeCategoryidx?#EEF3FD:transparent).onClick((){this.switchCategory(idx)})})}.backgroundColor(#F0F2F5).borderRadius(12)选中状态浅蓝背景 蓝字未选中透明背景 灰字。单位选择器这个有点意思我做成了横向滚动的标签Scroll(){Row(){ForEach(cat.units,(unit,idx){Text(unit.label).backgroundColor(this.fromIdxidx?#5B8DEF:#F0F2F5).onClick((){this.fromIdxidxthis.convert()})})}}.scrollable(ScrollDirection.Horizontal)选中的单位蓝色背景 白字未选中灰色背景。输入框TextInput({placeholder:输入数值,text:this.inputValue}).type(InputType.Number).onChange((val){this.inputValuevalthis.convert()// 实时换算})亮点每次输入都触发换算不需要点计算按钮输入即出结果。交换按钮中间放个按钮点击互换源和目标单位Button(){Text(⇅).fontSize(24)}.onClick((){consttmpthis.fromIdxthis.fromIdxthis.toIdxthis.toIdxtmpthis.convert()})第五天细节优化处理边界情况第二个坑浮点数精度测试时输入0.1 0.2结果显示0.30000000000000004。原因JavaScript 的经典浮点数精度问题。解决用toFixed限制精度再去掉多余的零formatResult(val){constfixedval.toFixed(6)returnparseFloat(fixed).toString()}第三个坑单位索引越界在长度类别选了第6个单位英尺切到温度类别时崩溃了。原因温度只有3个单位索引5越界了。解决切换类别时重置索引switchCategory(idx){this.activeCategoryidxthis.fromIdx0this.toIdxMath.min(1,CATEGORIES[idx].units.length-1)}极大极小数字比如1毫米 0.000001千米这显示起来太长了。解决科学计数法if(Math.abs(val)0.000001val!0){returnval.toExponential(4)}if(Math.abs(val)999999999){returnval.toExponential(4)}第六天测试各种边界情况总结学到了什么数据结构很重要好的数据结构能让算法简单很多边界情况要考虑越界、精度、极大极小值实时交互体验好输入即出结果比点按钮爽踩坑总结坑表现解决温度换算结果错误特殊函数处理浮点精度小数位太长toFixed parseFloat索引越界切换类别崩溃重置索引 Math.min极大极小显示太长科学计数法代码量最后统计了一下主文件Index.ets约 200 行。对于一个完整的应用来说这个量级很合适——既不复杂又有技术含量。开发时间2天核心代码~200行做菜时再也不用到处找换算工具了挺有成就感的