欢迎光临
我们一直在努力

HarmonyOS NEXT 实战:从零开发多功能单位换算器

HarmonyOS NEXT 实战:从零开发多功能单位换算器

包名:com.example.myapplication
API版本:HarmonyOS NEXT (API 23+)
开发工具:DevEco Studio


一、前言:为什么要做单位换算器?

在日常生活和工作中,单位换算是非常常见的需求:烹饪时需要换算重量、装修时需要换算面积、出国时需要换算温度……虽然手机上有很多换算工具,但作为开发者,自己动手实现一个单位换算器,不仅能解决实际问题,还能深入理解鸿蒙开发的诸多核心技术点。

这个项目涉及:

  • 数据结构设计:如何优雅地定义单位和换算规则
  • 状态管理:@State 装饰器管理多维度状态
  • 用户交互:Tab切换、单位选择、实时换算
  • 算法实现:通用换算算法 + 温度特殊处理
  • UI设计:卡片式布局、横向滚动选择器

本文将详细记录从设计到实现的完整过程,适合正在学习鸿蒙开发的同学参考。


二、功能设计

2.1 核心功能

功能模块描述
多类别支持 长度、重量、温度、面积四大类
实时换算 输入即出结果,无需点击按钮
单位切换 横向滚动选择源/目标单位
交换功能 一键互换源单位和目标单位
智能格式化 自动处理极大/极小数字

2.2 支持的单位

长度类
单位符号基准系数
毫米 mm 0.001
厘米 cm 0.01
米 m 1(基准)
千米 km 1000
英寸 in 0.0254
英尺 ft 0.3048
重量类
单位符号基准系数
毫克 mg 0.001
克 g 1(基准)
千克 kg 1000
吨 t 1,000,000
磅 lb 453.59237
盎司 oz 28.349523125
温度类
单位符号换算方式
摄氏度 °C 基准
华氏度 °F 特殊公式
开尔文 K 特殊公式
面积类
单位符号基准系数
平方毫米 mm² 0.000001
平方厘米 cm² 0.0001
平方米 m² 1(基准)
平方千米 km² 1,000,000
公顷 ha 10,000
英亩 acre 4046.8564224

2.3 UI设计

采用卡片式布局,整体风格简洁现代:

┌─────────────────────────────┐
│ 📐 单位换算器 │
├─────────────────────────────┤
│ 📏长度 │ ⚖️重量 │ 🌡️温度 │ 🔲面积 │ Tab栏
├─────────────────────────────┤
│ ┌─────────────────────┐ │
│ │ 从 │ │
│ │ [mm][cm][m][km]… │ │ 输入区
│ │ ┌─────────────────┐ │ │
│ │ │ 输入数值 │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
│ ⇅ │ 交换按钮
│ ┌─────────────────────┐ │
│ │ 到 │ │
│ │ [mm][cm][m][km]… │ │ 输出区
│ │ = 100 cm │ │ 结果
│ └─────────────────────┘ │
└─────────────────────────────┘


三、项目初始化

3.1 创建项目

打开 DevEco Studio,选择 Empty Ability 模板:

  • 项目名称:MyApplication
  • Bundle Name:com.example.myapplication
  • Compile SDK:API 23(HarmonyOS NEXT)
  • Model:Stage 模型

3.2 项目结构

MyApplication/
├── AppScope/
│ └── app.json5 # 应用配置
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets
│ │ └── pages/
│ │ └── Index.ets # 主页面 ⭐
│ ├── resources/
│ │ └── base/element/
│ │ └── string.json
│ └── module.json5
└── build-profile.json5


四、数据结构设计

4.1 单位信息接口

每个单位需要两个属性:名称和换算系数。

interface UnitInfo {
label: string // 显示名称,如 "米 (m)"
toBase: number // 换算到基准单位的系数
}

设计思路:

  • toBase 表示1个该单位等于多少个基准单位
  • 例如:厘米的 toBase = 0.01,表示 1cm = 0.01m
  • 千米的 toBase = 1000,表示 1km = 1000m

4.2 类别接口

每个换算类别包含名称、图标、单位列表,以及可选的特殊换算函数。

interface Category {
name: string // 类别名称
icon: string // 图标 emoji
units: UnitInfo[] // 单位列表
convertFn?: (val: number, fromIdx: number, toIdx: number) => number
}

为什么需要 convertFn?

大多数单位(长度、重量、面积)可以通过简单的乘除法换算,但温度不同——摄氏度、华氏度、开尔文之间的换算需要特殊公式。convertFn 就是处理这种情况的。

4.3 完整数据定义

const CATEGORIES: Category[] = [
{
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 },
]
},
{
name: '重量', icon: '⚖️',
units: [
{ label: '毫克 (mg)', toBase: 0.001 },
{ label: '克 (g)', toBase: 1 },
{ label: '千克 (kg)', toBase: 1000 },
{ label: '吨 (t)', toBase: 1_000_000 },
{ label: '磅 (lb)', toBase: 453.59237 },
{ label: '盎司 (oz)', toBase: 28.349523125 },
]
},
{
name: '温度', icon: '🌡️',
units: [
{ label: '摄氏度 (°C)', toBase: 0 },
{ label: '华氏度 (°F)', toBase: 0 },
{ label: '开尔文 (K)', toBase: 0 },
],
convertFn: (val: number, fromIdx: number, toIdx: number): number => {
// 先转摄氏度
let celsius: number
if (fromIdx === 0) celsius = val
else if (fromIdx === 1) celsius = (val – 32) * 5 / 9
else celsius = val – 273.15

// 再转目标单位
if (toIdx === 0) return celsius
if (toIdx === 1) return celsius * 9 / 5 + 32
return celsius + 273.15
}
},
{
name: '面积', icon: '🔲',
units: [
{ label: '平方毫米 (mm²)', toBase: 0.000001 },
{ label: '平方厘米 (cm²)', toBase: 0.0001 },
{ label: '平方米 (m²)', toBase: 1 },
{ label: '平方千米 (km²)', toBase: 1_000_000 },
{ label: '公顷 (ha)', toBase: 10_000 },
{ label: '英亩 (acre)', toBase: 4046.8564224 },
]
},
]


五、状态管理

5.1 状态变量定义

@Entry
@Component
struct UnitConverter {
// 当前选中的类别索引
@State activeCategory: number = 0

// 源单位索引
@State fromIdx: number = 0

// 目标单位索引
@State toIdx: number = 1

// 输入值
@State inputValue: string = ''

// 换算结果
@State resultValue: string = ''
}

设计思路:

  • activeCategory:控制当前显示哪个类别的单位
  • fromIdx / toIdx:选中单位的索引,用于从数据数组中取值
  • inputValue:用户输入的原始字符串
  • resultValue:换算后的结果字符串

5.2 状态联动

当用户切换类别时,需要重置单位索引:

switchCategory(idx: number): void {
this.activeCategory = idx
this.fromIdx = 0
this.toIdx = Math.min(1, CATEGORIES[idx].units.length – 1)
this.convert()
}

为什么用 Math.min?

因为不同类别的单位数量可能不同。温度只有3个单位,如果之前选了第5个单位,切换到温度会越界。


六、换算算法实现

6.1 通用换算算法

对于长度、重量、面积等线性单位,使用"基准单位法":

输入值 → 基准单位 → 目标单位

// 标准换算:输入 → 基准 → 输出
const base: number = num * cat.units[this.fromIdx].toBase
const res: number = base / cat.units[this.toIdx].toBase

示例:将 5 千米换算为米

  • 输入值:5
  • 转基准:5 * 1000 = 5000 米
  • 转目标:5000 / 1 = 5000 米
  • 6.2 温度特殊换算

    温度不能简单乘除,需要先转成摄氏度,再转成目标单位:

    convertFn: (val: number, fromIdx: number, toIdx: number): number => {
    // 第一步:转摄氏度
    let celsius: number
    if (fromIdx === 0) celsius = val // 摄氏度
    else if (fromIdx === 1) celsius = (val – 32) * 5 / 9 // 华氏度转摄氏度
    else celsius = val – 273.15 // 开尔文转摄氏度

    // 第二步:转目标单位
    if (toIdx === 0) return celsius // 摄氏度
    if (toIdx === 1) return celsius * 9 / 5 + 32 // 摄氏度转华氏度
    return celsius + 273.15 // 摄氏度转开尔文
    }

    换算公式:

    转换公式
    °C → °F C × 9/5 + 32
    °F → °C (F – 32) × 5/9
    °C → K C + 273.15
    K → °C K – 273.15

    6.3 完整换算函数

    convert(): void {
    const trimmed: string = this.inputValue.trim()
    if (trimmed.length === 0) {
    this.resultValue = ''
    return
    }

    const num: number = parseFloat(trimmed)
    if (isNaN(num)) {
    this.resultValue = '—'
    return
    }

    const cat: Category = CATEGORIES[this.activeCategory]

    if (cat.convertFn) {
    // 温度特殊换算
    const res: number = cat.convertFn(num, this.fromIdx, this.toIdx)
    this.resultValue = this.formatResult(res)
    } else {
    // 标准换算:输入 → 基准 → 输出
    const base: number = num * cat.units[this.fromIdx].toBase
    const res: number = base / cat.units[this.toIdx].toBase
    this.resultValue = this.formatResult(res)
    }
    }

    6.4 结果格式化

    处理各种边界情况:

    formatResult(val: number): string {
    if (isNaN(val) || !isFinite(val)) return '—'

    // 处理极小/极大数字
    if (Math.abs(val) < 0.000001 && val !== 0) return val.toExponential(4)
    if (Math.abs(val) > 999999999) return val.toExponential(4)

    // 四舍五入到合适精度
    const fixed: string = val.toFixed(6)

    // 去掉末尾多余的 0 和小数点
    return parseFloat(fixed).toString()
    }

    处理场景:

    情况示例处理方式
    极小数字 0.0000001 科学计数法 1e-7
    极大数字 10000000000 科学计数法 1e+10
    正常数字 1.23456789 四舍五入 1.234568
    多余零 1.500000 去零 1.5

    七、UI布局实现

    7.1 整体结构

    build() {
    Column() {
    // 标题
    Text('📐 单位换算器')
    .fontSize(26)
    .fontWeight(FontWeight.Bold)

    // 类别标签
    Row() {
    ForEach(CATEGORIES, …)
    }

    // 换算卡片
    Column() {
    // 输入区域
    Column() { … }

    // 交换按钮
    Column() { … }

    // 输出区域
    Column() { … }
    }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F7FA')
    }

    7.2 类别标签栏

    使用 ForEach 遍历类别数组:

    Row() {
    ForEach(CATEGORIES, (cat: Category, idx: number) => {
    Column() {
    Text(cat.icon)
    .fontSize(22)
    Text(cat.name)
    .fontSize(13)
    .fontColor(this.activeCategory === idx ? '#5B8DEF' : '#888888')
    .margin({ top: 3 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .borderRadius(10)
    .backgroundColor(this.activeCategory === idx ? '#EEF3FD' : 'transparent')
    .onClick(() => { this.switchCategory(idx) })
    })
    }
    .width('94%')
    .backgroundColor('#F0F2F5')
    .borderRadius(12)
    .padding(4)

    交互效果:

    • 未选中:透明背景,灰色文字
    • 选中:浅蓝背景,蓝色文字

    7.3 单位选择器

    横向滚动的单位选择器:

    Scroll() {
    Row() {
    ForEach(CATEGORIES[this.activeCategory].units, (unit: UnitInfo, idx: number) => {
    Text(unit.label)
    .fontSize(14)
    .fontColor(this.fromIdx === idx ? '#FFFFFF' : '#555555')
    .padding({ top: 6, bottom: 6, left: 14, right: 14 })
    .backgroundColor(this.fromIdx === idx ? '#5B8DEF' : '#F0F2F5')
    .borderRadius(16)
    .margin({ right: 8 })
    .onClick(() => {
    this.fromIdx = idx
    this.convert()
    })
    })
    }
    }
    .scrollable(ScrollDirection.Horizontal)
    .width('100%')
    .height(36)

    特点:

    • 选中项:蓝色背景 + 白色文字
    • 未选中:灰色背景 + 深灰文字
    • 支持横向滚动,适应多单位

    7.4 输入框

    TextInput({ placeholder: '输入数值', text: this.inputValue })
    .height(52)
    .width('100%')
    .type(InputType.Number)
    .backgroundColor('#F5F7FA')
    .borderRadius(12)
    .fontSize(22)
    .fontWeight(FontWeight.Bold)
    .fontColor('#333333')
    .placeholderColor('#CCCCCC')
    .margin({ top: 12 })
    .onChange((val: string) => {
    this.inputValue = val
    this.convert() // 实时换算
    })

    关键属性:

    • type(InputType.Number):数字键盘
    • onChange:每次输入都触发换算,实现实时效果

    7.5 交换按钮

    Column() {
    Button() {
    Text('⇅')
    .fontSize(24)
    }
    .width(44).height(44)
    .backgroundColor('#EEF3FD')
    .borderRadius(22)
    .onClick(() => { this.swapUnits() })
    }
    .padding({ top: 6, bottom: 6 })
    .alignItems(HorizontalAlign.Center)

    交换逻辑:

    swapUnits(): void {
    const tmp: number = this.fromIdx
    this.fromIdx = this.toIdx
    this.toIdx = tmp
    this.convert()
    }

    7.6 结果显示

    if (this.inputValue.trim().length > 0 && this.resultValue.length > 0) {
    Row() {
    Text('= ')
    .fontSize(28)
    .fontColor('#5B8DEF')
    .fontWeight(FontWeight.Bold)

    Text(this.resultValue)
    .fontSize(32)
    .fontColor('#333333')
    .fontWeight(FontWeight.Bold)

    Text(' ' + CATEGORIES[this.activeCategory].units[this.toIdx].label.replace(/^[^\\s]+\\s/, ''))
    .fontSize(16)
    .fontColor('#888888')
    .margin({ left: 4 })
    }
    .padding({ top: 14, bottom: 8 })
    } else {
    Text('请输入数值')
    .fontSize(16)
    .fontColor('#CCCCCC')
    .padding({ top: 18, bottom: 18 })
    }

    单位名处理:

    label.replace(/^[^\\s]+\\s/, '') 用于提取单位符号。

    例如:"米 (m)" → "m","摄氏度 (°C)" → "(°C)"


    八、运行效果

    在这里插入图片描述

    九、踩坑记录

    9.1 踩坑一:单位索引越界

    问题:在长度类别选了第6个单位(英尺),切换到温度类别时崩溃。

    原因:温度只有3个单位,索引5越界。

    解决:切换类别时重置索引,并检查边界:

    switchCategory(idx: number): void {
    this.activeCategory = idx
    this.fromIdx = 0
    this.toIdx = Math.min(1, CATEGORIES[idx].units.length – 1)
    }

    9.2 踩坑二:结果精度问题

    问题:输入"0.1 + 0.2"类似的运算,结果显示一长串小数。

    原因:JavaScript 浮点数精度问题。

    解决:使用 toFixed(6) 限制精度,再用 parseFloat 去除多余零:

    const fixed: string = val.toFixed(6)
    return parseFloat(fixed).toString()

    9.3 踩坑三:温度换算错误

    问题:温度换算结果不正确。

    原因:错误地使用了 toBase 系数法。

    解决:温度需要特殊处理,使用 convertFn 函数实现自定义换算逻辑。

    9.4 踩坑四:横向滚动不生效

    问题:单位选择器无法横向滚动。

    原因:外层 Row 没有设置正确的宽度约束。

    解决:确保 Scroll 有明确的宽高:

    Scroll() {
    Row() { … }
    }
    .scrollable(ScrollDirection.Horizontal)
    .width('100%')
    .height(36)

    9.5 踩坑五:数字键盘显示错误

    问题:输入框弹出的是全键盘而不是数字键盘。

    原因:没有指定 type 属性。

    解决:

    TextInput({ placeholder: '输入数值' })
    .type(InputType.Number)


    十、优化建议

    10.1 功能扩展

  • 更多单位类别:体积、速度、时间、货币等
  • 历史记录:保存用户最近使用的换算组合
  • 收藏功能:常用单位快速访问
  • 离线汇率:货币换算支持离线模式
  • 10.2 性能优化

  • 防抖处理:输入时使用防抖,减少换算频率
  • 缓存结果:相同输入避免重复计算
  • 懒加载:单位列表动态加载
  • 10.3 用户体验

  • 动画效果:类别切换、结果更新时添加过渡动画
  • 复制功能:一键复制换算结果
  • 错误提示:非法输入时给出友好提示

  • 十一、总结

    11.1 技术要点回顾

    类别知识点
    数据结构 interface定义、数组遍历
    状态管理 @State装饰器、状态联动
    算法实现 基准单位法、温度特殊换算
    UI组件 ForEach、Scroll、TextInput
    交互设计 Tab切换、横向滚动选择器

    11.2 项目亮点

    • ✅ 完整的单位换算逻辑(通用+特殊)
    • ✅ 实时换算,输入即出结果
    • ✅ 智能结果格式化
    • ✅ 简洁的卡片式UI设计
    • ✅ 横向滚动单位选择器

    11.3 代码统计

    • 核心代码:约 200 行(Index.ets 单文件)
    • 数据定义:约 50 行
    • 换算逻辑:约 30 行
    • UI布局:约 120 行

    十二、源码附录

    完整数据定义

    const CATEGORIES: Category[] = [
    {
    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 {
    const num = parseFloat(this.inputValue.trim())
    if (isNaN(num)) {
    this.resultValue = '—'
    return
    }

    const cat = CATEGORIES[this.activeCategory]
    if (cat.convertFn) {
    this.resultValue = this.formatResult(cat.convertFn(num, this.fromIdx, this.toIdx))
    } else {
    const base = num * cat.units[this.fromIdx].toBase
    const res = base / cat.units[this.toIdx].toBase
    this.resultValue = this.formatResult(res)
    }
    }


    赞(0)
    未经允许不得转载:171主机测评 » HarmonyOS NEXT 实战:从零开发多功能单位换算器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址