实验5:鸿蒙开发入门及计算器开发
鸿蒙ArkTS科学计算器开发实战:从单文件玩具到模块化专业级应用的五轮迭代
中国海洋大学 2026 夏季小学期《移动软件开发》实验5
开发环境:DevEco Studio 26.0.0 + HarmonyOS 7.0 (API 26)
项目架构:ArkTS 声明式UI + 模块化分层设计
前言
这学期的移动软件开发课,前面几个实验都是微信小程序开发,终于到了鸿蒙(HarmonyOS)环节。说实话,一开始听到"鸿蒙开发"还有点发怵——新系统、新语言(ArkTS)、新IDE(DevEco Studio),一切都要从零开始。
但真正动手做下来才发现,鸿蒙的声明式UI开发范式其实非常优雅。而且这次实验我没有满足于"能算加减乘除就行",而是前前后后迭代了五轮,最终做出了一个支持完整表达式求值、4套主题、历史记录持久化、常量库、自定义按键动画的专业级科学计算器,代码也从最初的单文件800行重构为9个模块文件的分层架构。
这篇博客就来记录整个开发过程中的思考、踩坑和收获,希望能帮到同样在学鸿蒙开发的同学。
一、环境搭建:第一个拦路虎是Hyper-V
1.1 安装DevEco Studio
从华为开发者联盟官网下载 DevEco Studio 26.0.0 Release,安装过程和 Android Studio 差不多(毕竟都是基于 IntelliJ 平台),一路下一步就行。
1.2 创建模拟器,踩坑Hyper-V
创建项目之前得先有个模拟器。Tools → Device Manager → New Emulator,选 Huawei_Phone 模板,下载 HarmonyOS 7.0 系统镜像。但点启动的瞬间,报错了:
ErrorCode: 00801001
未开启Hyper-V。
运行模拟器需要开启Hyper-V虚拟化支持。
行吧,鸿蒙模拟器吃 Hyper-V。我以管理员身份打开 PowerShell,执行:
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
结果又报错:功能名称 Microsoft-Hyper-V 未知。
查了一下才知道,Windows 家庭版默认不带完整的 Hyper-V 组件。但好消息是,鸿蒙模拟器不一定需要完整 Hyper-V,只要启用 Windows Hypervisor Platform 和 虚拟机平台 就够了:
Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform -All
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -All
执行完重启电脑,模拟器终于能正常启动了,看到 HarmonyOS 的开机画面那一刻还是有点小激动的。
💡 踩坑总结:Windows 家庭版用户不用慌,不需要升级到专业版,启用 HypervisorPlatform + VirtualMachinePlatform 就能跑鸿蒙模拟器。
1.3 Hello World验证环境
环境通了之后,创建一个 Empty Ability 模板的工程,点运行,模拟器上出现 Hello World——环境搭建完成,正式开始写计算器。
二、第一轮:基础计算器 + ArkTS语法踩坑
2.1 ArkTS初体验
ArkTS 是 TypeScript 的严格超集,采用声明式 UI 开发。一个页面的基本结构长这样:
@Entry
@Component
struct Index {
@State display: string = '0'
build() {
Column() {
Text(this.display)
Button('点击我')
.onClick((): void => {
this.display = 'Hello'
})
}
}
}
几个核心概念:
@Entry:页面入口@Component:自定义组件@State:状态变量,值变了自动刷新UIbuild():UI描述方法,返回组件树Column/Row:纵向/横向布局容器
这种"数据驱动UI"的范式和 Flutter、React 很像,但 ArkTS 的类型检查更严格。
2.2 第一次编译,22个错误
代码写完点运行,直接报了 22 个编译错误,主要是三类:
错误1:arkts-no-implicit-return-types
所有 onClick(() => this.clear()) 形式的箭头函数全部报错。ArkTS 要求箭头函数必须显式声明返回类型,改成 (): void => { this.clear() } 就好了。
错误2:FontWeight.Light 不存在
ArkTS 的 FontWeight 枚举里没有 Light,只有 Lighter、Normal、Medium、Bold 等,改成 FontWeight.Lighter。
错误3:Button 没有 textAlign 属性
只有 Text 组件支持 textAlign,Button 不支持,直接删掉。
💡 踩坑总结:ArkTS 的 API 和其他框架有差异,不能凭经验想当然,遇到不确定的属性查官方文档最靠谱。
修完这 22 个错误,基础计算器终于跑起来了,能算加减乘除了。
三、第二轮:布局优化与界面美化
基础功能有了,但界面实在是……朴素。于是开始第二轮迭代,重点解决布局问题和美化界面。
3.1 布局踩坑:按钮变成了横向长条
一开始给按钮设置 width('80%'),结果四个按钮在一行里每个都占父容器 80% 的宽度,直接溢出屏幕,按钮被拉成了横向长条,右边的按钮还被截断了。
解决方法:改用 layoutWeight(1),让按钮在 Row 里平均分配剩余宽度。0 按钮要占两格就设 layoutWeight(2),完美解决。
// ❌ 错误写法
Button('7').width('80%')
// ✅ 正确写法
Button('7').layoutWeight(1).height(55)
3.2 界面美化:深色渐变 + 玻璃拟态
布局修好之后开始美化,最终定了一套深色科技风:
- 背景:深紫蓝三色渐变
- 显示屏:玻璃拟态效果——半透明白色背景 + 细边框 + 大阴影 + 结果文字青色辉光
- 按钮:145° 渐变 + 圆角 + 投影
// 玻璃拟态显示屏
Column() {
Text(this.history)
Text(this.display)
.shadow({
radius: 10,
color: 'rgba(0, 229, 255, 0.2)',
offsetX: 0,
offsetY: 0
})
}
.width('88%')
.backgroundColor('rgba(255, 255, 255, 0.06)')
.borderRadius(18)
.border({ width: 1, color: 'rgba(255, 255, 255, 0.1)' })
3.3 又踩一坑:stateStyles按下不恢复
想给按钮加按下变暗的效果,用了 .stateStyles({ pressed: { .opacity(0.65) } }),结果点击后按钮一直保持半透明,不恢复了。查了一下是 ArkTS 的 stateStyles 在 @Builder 自定义组件里有 bug。
这个问题后来在第四轮迭代中通过封装自定义按键组件彻底解决了,按下效果反而做得更丰富了。
四、第三轮:升级为科学计算器
基础计算器 + 美化完成后,我觉得还不够——既然叫"计算器",不如直接做成专业级的科学计算器。于是第三轮迭代,加入了一大堆专业功能。
4.1 2nd第二功能键:一键双功能的经典设计
专业科学计算器(卡西欧、TI 系列)都有一个 2nd 键,按下后所有函数切换为第二功能(sin→sin⁻¹、ln→eˣ、√→∛ 等)。我也实现了这个设计:
@State is2nd: boolean = false
// 按钮文字根据 is2nd 动态切换
this.CalcButton(
this.is2nd ? 'sin⁻¹' : 'sin',
...
(): void => { this.applyUnary(this.is2nd ? 'asin' : 'sin') }
)
2nd 键按下后高亮(青色背景 + 黑色文字),而且是一次性开关——按下任意功能键后自动复位(2nd 键自身除外),符合专业计算器的交互习惯。
完整的 2nd 功能映射:
| 默认功能 | 2nd模式 |
|---|---|
| sin | sin⁻¹ (反正弦) |
| cos | cos⁻¹ (反余弦) |
| tan | tan⁻¹ (反正切) |
| ln | eˣ (e的x次方) |
| log | 10ˣ (10的x次方) |
| √ | ∛ (立方根) |
| x² | x³ (立方) |
| 1/x | 2ˣ (2的x次方) |
| M+ | M− (内存减) |
| n! | Rand (随机数) |
一下子多了 10 个功能,但按钮数量没变——这就是 2nd 键的魅力。
4.2 内存操作:MC/MR/M+/M−
专业计算器标配的内存功能也安排上了:
- MC(Memory Clear):清除内存
- MR(Memory Recall):读取内存值
- M+(Memory Add):当前值加到内存
- M−(Memory Subtract):从内存减去当前值(2nd切换)
4.3 角度三态切换:RAD/DEG/GRAD
三角函数的结果取决于角度单位,我实现了三态切换:
- RAD:弧度(默认)
- DEG:角度
- GRAD:梯度
点击右上角胶囊循环切换,三角函数计算时自动转换:
toRadian(value: number): number {
if (this.angleMode === 1) return value * Math.PI / 180 // DEG
if (this.angleMode === 2) return value * Math.PI / 200 // GRAD
return value
}
反三角函数的结果也要反向转换回来,细节拉满。
4.4 其他细节
- Ans:调用上一次计算结果,方便链式计算
- Rand:生成 0-1 随机数
- ±:正负号切换
- n!:阶乘(支持 0-170 整数范围,超出返回 Error)
- xʸ:任意次幂(如 2^10=1024)
- 错误处理:除零、负数开平方、负数对数都返回 Error
五、第四轮:架构重构——从单文件玩具到模块化专业应用
科学功能加完之后,新问题来了——Index.ets 已经膨胀到 1000 多行,计算逻辑、UI布局、格式化、按键定义全混在一起,修改一处容易影响其他部分。
于是我做了一个重要决定:全面重构,模块化分层。
5.1 模块化架构设计
按职责将代码拆分为 5 个目录、9 个文件:
entry/src/main/ets/
├── pages/
│ └── Index.ets // 主页面(状态管理 + 按键分发)
├── engine/
│ ├── Evaluator.ets // 表达式求值引擎(双栈算法,纯函数)
│ └── Formatter.ets // 数字格式化(千分位、科学计数法上标、自适应字号)
├── components/
│ └── CalcKeyButton.ets // 自定义按键组件(按下动画、长按回调)
├── common/
│ ├── AppTheme.ets // 主题系统(4套主题定义)
│ └── KeyModel.ets // 按键模型(数据驱动布局,2nd第二功能)
├── model/
│ ├── HistoryRecord.ets // 历史记录模型
│ └── ConstItem.ets // 常量库模型(数学常数+物理常数)
└── utils/
├── Prefs.ets // 偏好设置持久化
└── Haptic.ets // 震动反馈(20ms节流)
这种分层架构遵循"单一职责"原则:计算逻辑(engine)、UI组件(components)、数据模型(model)、工具函数(utils)、公共配置(common)各司其职,主页面(pages)只负责状态管理和按键分发。
5.2 核心亮点1:双栈表达式求值引擎
初版采用的是"输入一个数→点运算符→保存当前值→输入下一个数→点等号"的即时计算模式,无法支持括号和运算符优先级。重构时我实现了一个完整的双栈表达式求值引擎。
算法采用经典的"双栈 + 期望操作数"状态机:
- 一个值栈(values)存放操作数
- 一个运算符栈(ops)存放待运算的运算符
- 用
expectOperand状态区分一元负号和二元减号 - 通过比较运算符优先级决定入栈还是弹出运算
支持的特性:
- 运算符优先级:×÷高于±,^(幂)最高且右结合
- 圆括号嵌套:支持任意层级括号
- 一元负号:如 -3、-(-2),与二元减号自动区分
- 后缀运算符:百分号(%)与阶乘(!)
- 错误处理:语法错误、括号不匹配、除零、超定义域分别返回不同错误信息
// 运算符优先级,数值越大越先算
function precedence(op: string): number {
switch (op) {
case '+': case '-': return 1
case '×': case '÷': return 2
case 'neg': case '%': return 3
case '^': return 4
case '!': return 5
default: return 0
}
}
所有对外接口均为纯函数,不持有状态,便于单元测试和维护。这次重构让计算器从"玩具级"升级为"专业级"。
同时增加了实时预览功能:输入过程中(token 数 ≥ 3 时)实时显示当前表达式的预览结果(“= xxx”),无需按等号即可看到计算结果。
5.3 核心亮点2:自定义按键组件
放弃了有 bug 的 stateStyles 方案,封装了独立的 CalcKeyButton 组件,自带完整的交互反馈:
- 按下态动画:按下时透明度降至0.78、缩放至0.94、阴影半径缩小且偏移减小,抬起时通过 130ms 缓动动画恢复
- 长按支持:按住 550ms 触发长按回调,长按触发后不再触发点击回调
- 状态隔离:按下状态放在组件内部(@State private pressed),避免父组件因按键态变化而整体重建
@Component
export struct CalcKeyButton {
@Prop label: string = ''
@Prop colorStart: string = '#2C2C4A'
@Prop colorEnd: string = '#1B1B33'
// ... 其他属性
onTap: () => void = (): void => {}
onLongTap: () => void = (): void => {}
@State private pressed: boolean = false
build() {
Text(this.label)
.opacity(this.pressed ? 0.78 : 1)
.scale({ x: this.pressed ? 0.94 : 1, y: this.pressed ? 0.94 : 1 })
.animation({ duration: 130, curve: Curve.EaseOut })
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.pressed = true
// 长按计时...
} else if (event.type === TouchType.Up) {
this.pressed = false
if (!this.longFired) { this.onTap() }
}
})
}
}
5.4 核心亮点3:4套主题系统
实现了完整的多主题系统,界面上不允许出现硬编码颜色,全部从当前主题取值。每套主题包含 20 余项配置(背景渐变、玻璃卡片、三级文字色、强调色、四类按键渐变、投影色、浮层色等)。
内置 4 套主题:
| 主题名 | 风格 | 强调色 |
|---|---|---|
| 深空霓虹 | 暗夜里的赛博光晕 | 青色 #22E6FF |
| 极光森林 | 清透的青绿薄雾 | 青绿 #5EEAD4 |
| 暮色霞光 | 落日熔金的暖调 | 暖橙 #FFB07C |
| 瓷白 | 明亮通透的日间模式 | 蓝紫 #3D5AFE |
新增一套皮肤只需在 buildThemes() 中加一个条目,无需修改任何 UI 代码。主题选择通过底部弹窗切换,并持久化到本地存储。
5.5 核心亮点4:数字格式化引擎
负责输入串和结果的显示格式化:
- 千分位分组:输入时自动插入千分位逗号(1234567 → 1,234,567)
- 科学计数法上标美化:结果超范围自动转科学计数法,指数用上标字符显示(3.14×10⁸)
- 自适应字号:根据文本长度自动挑选字号(6字以内44号,24字以上18号),保证长数字不被截断
5.6 核心亮点5:历史记录与常量库
- 历史记录:每次按等号后保存表达式、结果、时间,持久化到本地。点击记录可复用结果,支持清空。
- 常量库:内置 14 个常用常量,分为数学常数(π、e、φ黄金比例、√2、ln2、γ欧拉常数)和物理常数(c真空光速、g重力加速度、G引力常数、h普朗克常数、e⁻元电荷、NA阿伏伽德罗常数、R摩尔气体常数、k玻尔兹曼常数),点击直接填入。
5.7 核心亮点6:持久化存储
通过 Prefs 工具类封装 @kit.ArkData 的 preferences API,实现主题选择、角度模式、科学区状态、历史记录的持久化。所有读写包在 try/catch 中,即便存储不可用也退化为纯内存运行,不影响主流程。
六、第五轮:细节打磨——让应用"活"起来
功能和架构都到位之后,最后一轮迭代专注于交互细节的打磨,让应用从"能用"变成"好用"。
6.1 震动反馈
每次按键触发 12ms 轻震动,让点击有实体感。20ms 内重复触发会被合并,避免连点时马达过度抖动。设备不支持时静默降级。
export class Haptic {
static tick(): void {
try {
const now: number = new Date().getTime()
if (now - lastTick < 20) return // 20ms节流
lastTick = now
vibrator.startVibration({ type: 'time', duration: 12 }, { id: 0, usage: 'touch' })
} catch (e) { /* 不支持时忽略 */ }
}
}
6.2 结果滑入动画
按等号后结果从下方轻微滑入并回弹(280ms 缓动),同时显示区缩放至 0.955 再恢复,增强计算完成的仪式感。
6.3 背景氛围光斑
界面左上角和右下角各有一个模糊光晕(blur 80),颜色随主题强调色变化,营造氛围感。
6.4 其他细节
- Toast 提示:操作反馈(如"内存已清除")通过底部 Toast 显示,1.6秒后自动消失
- 待命运算符高亮:当前表达式末尾落在运算符上时,对应运算符按键高亮显示
- 未闭合括号计数:实时统计未闭合的括号数量,显示在界面上
- 科学区滚动:科学模式展开后内容超出屏幕,最外层 Scroll 包裹,隐藏滚动条 + 弹性回弹
七、踩坑总结
整个开发过程踩了不少坑,在这里集中总结一下:
| 坑 | 解决方案 |
|---|---|
| Windows家庭版没有Hyper-V | 启用 HypervisorPlatform + VirtualMachinePlatform 即可 |
| 箭头函数编译报错 | 显式声明返回类型 (): void => { ... } |
| FontWeight.Light不存在 | 改用 FontWeight.Lighter |
| Button没有textAlign属性 | 删掉该属性,只有Text支持 |
| width(‘80%’)导致布局错乱 | 改用 layoutWeight(1) 平均分配宽度 |
| stateStyles按下不恢复 | 封装自定义按键组件,内部管理pressed状态 |
| 科学模式展开后按钮被遮挡 | 最外层Scroll包裹,隐藏滚动条+弹性回弹 |
| 即时计算不支持括号 | 重构为双栈表达式求值引擎 |
| 单文件代码膨胀难维护 | 模块化分层重构,拆分为9个文件 |
八、收获与思考
这次鸿蒙计算器开发,是我第一次接触国产操作系统的应用开发,收获远超预期:
1. 声明式UI开发范式的魅力
ArkTS 的 @State + 声明式布局,让"数据变了UI自动刷新"成为自然,不需要手动操作控件引用。写惯了命令式UI再写声明式,会有种"原来可以这么简洁"的感觉。
2. 模块化架构不是银弹,但没有银弹不行
初版单文件 1000 多行,修改计算逻辑时不小心影响了 UI 布局,排查困难。重构为分层架构后,各模块职责单一、接口清晰,新增功能只需修改对应模块。尤其是将表达式求值引擎设计为纯函数模块,不持有任何状态,不仅便于测试和维护,也让主页面的状态管理变得简洁。这次实践让我真正理解了"高内聚、低耦合"不是书本上的空话。
3. 好的算法是功能的基础
从"即时计算"升级为"双栈表达式求值",让计算器从"玩具级"变成了"专业级"。亲手实现一遍双栈算法,比看书理解深刻得多——尤其是用"期望操作数"状态区分一元负号和二元减号这个技巧,非常巧妙。
4. 迭代开发比一步到位更高效
计算器从基础版到专业科学计算器,经过了五轮迭代。每一轮都聚焦解决当前最突出的问题,功能和体验螺旋上升。如果一开始就想"一步到位做个完美的科学计算器",大概率会因为复杂度太高而卡住。
5. 细节决定体验
震动反馈、结果滑入动画、背景光晕、实时预览、待命运算符高亮、千分位分组、科学计数法上标……这些细节单个看都不起眼,但组合起来让应用的体验提升了一个档次。做移动端开发,细节真的很重要。
6. 鸿蒙开发生态已经相当成熟
DevEco Studio 的开发体验、ArkTS 的语言设计、模拟器的运行速度,都超出了我的预期。声明式UI + 严格类型检查 + 丰富的组件库,开发效率很高。作为国产操作系统,鸿蒙的应用开发生态已经具备了和主流平台掰手腕的实力,未来可期。
写在最后
鸿蒙开发比我想象中更有趣,ArkTS 写起来也很顺手。期待后续能学到更多鸿蒙的高级特性,比如分布式能力、原子服务、自定义组件封装等等。
如果这篇博客对你有帮助,欢迎点赞收藏~有问题也可以在评论区交流!
项目信息:
- 课程:中国海洋大学 2026 夏季小学期《移动软件开发》
- 实验:实验5 鸿蒙开发入门及计算器开发
- 开发环境:DevEco Studio 26.0.0 + HarmonyOS 7.0 (API 26)
- 开发语言:ArkTS
- 项目架构:模块化分层设计(engine/components/common/model/utils)
- 核心功能:双栈表达式求值、2nd第二功能、内存操作、4套主题、历史记录持久化、常量库、自定义按键动画、震动反馈
更多推荐
所有评论(0)