ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸿蒙ArkTS实战:空调控制页面开发全解析

鸿蒙ArkTS实战:空调控制页面开发全解析 1. 写在前面为什么要把空调控制做进鸿蒙应用这次要拆解的是一个非常典型的鸿蒙应用场景——空调控制页面。别小看这个页面它几乎涵盖了鸿蒙开发里所有常用基础能力UI布局、状态管理、数据绑定、组件交互、多设备适配。很多新手朋友一上来就想做大而全的项目结果卡在环境搭建和架构选型上反而把最核心的界面逻辑搁置了。我始终觉得空调控制页是一个绝佳的“全要素练习场”。先说清楚这篇文章不是教你调用某个现成的空调SDK也不是对接某个智能家居平台而是把“控制页面”本身从零到一写明白界面长什么样、状态怎么管理、温度风速模式怎么调整、怎么做到像真实家电一样可交互。你拿到这份内容后可以把它当成模板迁移到空气净化器、热水器、新风系统甚至任何带状态反馈的设备控制页上去。鸿蒙这边的技术栈是ArkTS ArkUI用的是声明式UI写法跟Flutter、SwiftUI在思路上有不少相似之处但又有自己的一套规则。如果你之前写过前端或者小程序上手会非常快如果是纯安卓开发出身可能需要先适应一下状态管理和UI描述方式。这篇文章适合这几类人看刚开始学鸿蒙应用开发、想找一个完整可运行的实战案例的人已经在写HarmonyOS应用、但想把页面交互细节做得更扎实的人以及做智能家居方向、经常要跟设备控制面板打交道的人。内容上我会从设计思路、数据模型、页面布局、交互逻辑、多设备适配一路写到问题排查全程配合代码讲解。2. 项目整体设计与核心思路拆解2.1 空调控制页到底在控制什么在写代码之前先得把需求理清楚。一套完整的空调控制页通常包含这么几个核心控制维度开关机状态整个设备的运行总开关也是页面最核心的展示信息。温度设定用户期望达到的目标温度而不是当前室温。运行模式常见的包括制冷、制热、送风、除湿有些高端机型还有自动模式。风速档位低风、中风、高风或者自动风。扫风方向上下扫风、左右扫风部分页面会做成按钮开关。辅助功能定时关机、睡眠模式、节能模式、健康换气等一般做成按钮列表。在鸿蒙页面上这六个维度的呈现优先级不一样。第一优先级必须是开关和温度因为这是用户每天按得最多的第二优先级是模式和风速属于“要调但不用一直看着”的选项辅助功能放在最底部需要的时候能快速找到平时不占据视觉重心。我见过不少开发者在做这类页面时犯同一个错误把空调所有功能都平铺在页面上结果看起来像个功能清单用户体验很差。好的控制页面要懂得做减法——把高频操作做大做醒目把低频功能收拢起来。2.2 状态驱动一个页面核心就是一套数据模型鸿蒙的ArkUI是声明式范式核心逻辑是页面由状态驱动。你定义好数据模型修改数据UI自动跟着变化。以空调页面为例我建议把设备的状态抽象成一个独立的数据类。这样做有两个好处一是页面代码干净只负责把状态渲染出来二是后续如果真接入了设备通信层比如Wi-Fi模块、蓝牙模块收到的数据直接更新这个状态对象页面就自动刷新了完全不用动UI代码。另外要考虑的是状态放在哪里比较合适。空调控制页这种场景页面内状态用State装饰器就够了如果多个页面共享同一个设备状态比如首页展示空调摘要、二级页做详细控制那就应该提升到Provide和Consume或者干脆用AppStorage做全局持久化。我的习惯是先做页面级别确定一切正常之后再根据业务需要往上提。过早引入全局状态管理只会增加调试成本。2.3 页面布局方案怎么选ArkUI里的布局容器用过几种Row、Column、Flex、Grid、Stack、RelativeContainer等。空调控制页整体上是“从上到下”的浏览顺序所以外层用Column里面按区域拆分顶部状态栏区域显示设备名称、连接状态、开关按钮。中部温度展示区这是一个横向大圆环或一个大数字温度值下面配加减按钮。模式区一排横向滚动的模式选项。风速区域用一组按钮或者滑块表达。底部辅助功能网格。温度区是整个页面视觉重心我用的是Stack式布局底层是一个圆弧轨道中间盖一个实心圆弧两端对齐形成环形进度条效果。这样做的好处是不依赖图片资源纯组件绘制适配不同屏幕尺寸非常稳。如果不想自己画环也可以用Canvas或者Progress组件但灵活性差一些。2.4 自适应延伸一个页面跑遍手机平板和车机鸿蒙的最大的卖点之一是“一次开发多端部署”。空调控制场景恰恰是跨设备高频场景手机可以远程控制平板可以当家庭中控车机在停车时也能开空调。所以布局方案上我会优先考虑“栅格断点”的响应式策略。ArkUI提供了GridRow和GridCol配合断点sm、md、lg来调整列数。比如手机上温度区占满一行模式区一行四个到了平板宽度可以改成温度区占左半侧其他功能占右半侧。这不是后期优化而是从设计之初就要留出余地。当然刚开始做的时候不用铺太开先把“窄屏优先”做好用breakpoints监听宽度在关键节点做布局切换即可。3. 项目环境准备与数据模型构建3.1 开发环境与工程创建鸿蒙应用开发目前主推DevEco Studio。去官网下载最新稳定版然后按引导把HarmonyOS SDK装好。创建工程时选择Empty Ability模板语言选ArkTS设备类型勾选Phone和Tablet。如果你的开发机是Windows记得把本地模拟器或远程真机配置好后面跑预览效率高很多。这里插一句工程创建后项目结构里有entry/src/main/ets目录页面代码放在pages下面我建议所有跟业务相关的数据模型单独放在model目录里入口是Index.ets。别图省事把所有代码都堆在一个文件里后面调试的时候你就知道这有多重要。我这次示例工程里核心文件就三个model/AirConditioner.ets——设备状态模型pages/Index.ets——页面入口pages/AirControlPage.ets——空调控制页主体3.2 设备数据模型定义设备模型我建议采用一个普通的类包含设备基础信息和所有可控制状态。代码如下// model/AirConditioner.ets export enum AirMode { Cool 制冷, Heat 制热, Fan 送风, Dry 除湿, Auto 自动 } export enum WindSpeed { Auto 自动, Low 低风, Mid 中风, High 高风 } export class AirConditioner { deviceName: string 客厅空调; isOn: boolean false; currentTemp: number 26; // 当前显示温度设定温度 targetTemp: number 26; // 目标温度 mode: AirMode AirMode.Cool; windSpeed: WindSpeed WindSpeed.Auto; sweepUpDown: boolean false; timerHours: number 0; // 0表示未设置定时 energySaving: boolean false; reset(): void { this.isOn false; this.targetTemp 26; this.mode AirMode.Cool; this.windSpeed WindSpeed.Auto; this.sweepUpDown false; this.timerHours 0; this.energySaving false; } }注意我把currentTemp和targetTemp分开了。真实场景里刚开机时显示的是环境温度随后逐步过渡到目标温度没有通信模组时currentTemp就默认为目标温度以免用户困惑。这个细节看似没多大影响但接真实设备时能帮你少改很多代码。3.3 页面状态绑定在页面类里定义状态变量State ac: AirConditioner new AirConditioner(); State tempRingProgress: number 0.5; // 温度环进度0~1这样在UI里使用this.ac.targetTemp、this.ac.mode等字段修改它们时页面会同步刷新。实际写下来你会发现状态管理是这类页面的核心命脉。你不需要手动去findViewById然后setText直接改数据UI自动响应。这就是声明式UI的爽点。4. 核心代码实现与操作详解4.1 页面总体结构空调控制页的整体结构用Column搭起来外层加一个安全的padding防止内容贴边或撞到系统状态栏。页面背景我选择了深色底因为智能家居控制面板普遍采用深色UI温度环和白色文字在深色背景下对比度好视觉效果也显得有质感。代码如下// pages/AirControlPage.ets import { AirConditioner, AirMode, WindSpeed } from ../model/AirConditioner; Entry Component struct AirControlPage { State ac: AirConditioner new AirConditioner(); State tempProgress: number 0.5; build() { Column() { // 页面标题栏 this.HeaderArea() // 温度环形显示与调节 this.TemperatureArea() // 模式选择 this.ModeSelector() // 风速与扫风 this.WindArea() // 辅助功能 this.FunctionGrid() // 底部安全区 Blank() } .width(100%) .height(100%) .padding({ left: 20, right: 20, top: 16, bottom: 16 }) .backgroundColor(#0D1117) } }别急着把所有代码塞进build()我的经验是每个区域封装成一个独立的Builder方法或Componet子组件这样页面看起来一目了然改起来也快。4.2 头部设备信息与开关头部信息包括设备名称、当前连接状态图标、开关按钮。Builder HeaderArea() { Row() { Column() { Text(this.ac.deviceName) .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor(#FFFFFF) Text(this.ac.isOn ? 运行中 : 已关机) .fontSize(12) .fontColor(this.ac.isOn ? #4ADE80 : #9CA3AF) } .alignItems(HorizontalAlign.Start) Blank() Toggle({ type: ToggleType.Switch, isOn: this.ac.isOn }) .selectedColor(#007DFF) .onChange((isOn: boolean) { this.ac.isOn isOn; if (isOn) { // 开机时可恢复上次设定也可以重置到默认值 this.ac.mode AirMode.Cool; } }) } .width(100%) }Toggle组件是鸿蒙内置的开关组件比自定义Switch省事很多。注意onChange回调的参数是布尔值记得重新赋值给this.ac.isOn。很多新手第一次写会奇怪“为什么开关没反应”多半是忘了这行赋值。这里还有一个细节关机状态下温度环和按钮应该呈灰色不可操作状态。所以后面所有交互组件我都加了enabled(this.ac.isOn)控制。4.3 温度显示与控制——环形进度条温度区是整个页面的灵魂。展示上我用一个大号Text显示当前设定温度旁边加摄氏度的单位下方放一排加减按钮。为了视觉丰富背后画一个环形进度代表当前温度在16到30度之间的相对位置。温度数值和进度条的映射关系const TEMP_MIN: number 16; const TEMP_MAX: number 30; private getTempProgress(temp: number): number { return (temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN); }比如设定26度进度就是(26-16)/(30-16)0.714约等于71%。环形进度我用Stack叠两层圆角矩形实现外层是轨道内层是进度圆弧。这里直接使用CirclestrokeDasharray可能参差不齐我优先用Canvas画。但考虑到部分读者对Canvas不熟我换一种方式——用Progress组件的type改成ProgressType.RingProgress({ value: this.ac.targetTemp, total: 14, type: ProgressType.Ring }) .width(180) .height(180) .color(#007DFF) .backgroundColor(#1E293B) .style({ strokeWidth: 8 })这里value直接传targetTemp - TEMP_MINtotal传TEMP_MAX - TEMP_MIN就免去了手动计算进度的麻烦。Progress组件很适合做这种环形指示简单、稳定、代码少。圆环正中央覆盖温度数字Stack({ alignContent: Alignment.Center }) { Progress({...}) Column() { Text(${this.ac.targetTemp}°) .fontSize(52) .fontWeight(FontWeight.Bold) .fontColor(#FFFFFF) Text(this.ac.mode) .fontSize(14) .fontColor(#9CA3AF) } }4.4 温度的加减与连续调节温度加减有两个方案一是按钮点击按1度步进二是用Slider滑动连续调节。我倾向于这两个都给用户保留按钮适合精确调整滑动条适合快速设定。按钮实现Row() { Button(-) .width(44) .height(44) .onClick(() { this.changeTemp(-1); }) Blank() Button() .width(44) .height(44) .onClick(() { this.changeTemp(1); }) }对应方法changeTemp(delta: number) { if (!this.ac.isOn) return; let newTemp this.ac.targetTemp delta; if (newTemp TEMP_MIN || newTemp TEMP_MAX) return; this.ac.targetTemp newTemp; }这里一定要加边界判断。空调温度范围一般是16到30度超过这个范围要么提示、要么静默拦截。我习惯直接拦截——真实空调控制里超范围设定本来就不允许。滑动条方案Slider({ value: this.ac.targetTemp, min: TEMP_MIN, max: TEMP_MAX, step: 1, style: SliderStyle.OutSet }) .width(80%) .onChange((value: number, mode: SliderChangeMode) { if (mode SliderChangeMode.Moving || mode SliderChangeMode.End) { this.ac.targetTemp value; } })注意SliderChangeMode判断。鸿蒙的Slider在拖动过程中会触发多次onChange如果每次都去刷新状态可能造成中间值闪烁所以我在Moving和End阶段才更新数据。实测下来这样手感最顺。4.5 运行模式选择模式选择的典型交互是一排横向滚动胶囊按钮。用户点某个模式高亮状态切换。Builder ModeSelector() { Column() { Text(运行模式).fontSize(16).fontColor(#E5E7EB) Scroll() { Row({ space: 12 }) { ForEach( [AirMode.Cool, AirMode.Heat, AirMode.Fan, AirMode.Dry, AirMode.Auto], (mode: AirMode) { Text(mode) .fontSize(14) .fontColor(this.ac.mode mode ? #FFFFFF : #9CA3AF) .padding({ left: 18, right: 18, top: 8, bottom: 8 }) .backgroundColor(this.ac.mode mode ? #007DFF : #1E293B) .borderRadius(20) .onClick(() { if (!this.ac.isOn) return; this.ac.mode mode; }) } ) } } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .width(100%) } .alignItems(HorizontalAlign.Start) .margin({ top: 20 }) }有些设备在送风模式下不允许调温度这是产品逻辑问题。我这里不打算把代码写得太死你在实际项目里如果需要联动可以在ModeSelector的onClick里加分支判断比如if (mode AirMode.Fan) { // 送风模式不改变温度状态但页面可以置灰温度调节控件 this.ac.mode mode; }4.6 风速与扫风控制风速我用两个方案供选择一排四个小按钮自动、低、中、高或者一个分段滑块。按钮更直观尤其适合触屏操作。这里用Row包四个Text选中高亮。Builder WindArea() { Column() { Text(风速).fontSize(16).fontColor(#E5E7EB) Row({ space: 10 }) { ForEach( [WindSpeed.Auto, WindSpeed.Low, WindSpeed.Mid, WindSpeed.High], (speed: WindSpeed) { Text(speed) .fontSize(13) .fontColor(this.ac.windSpeed speed ? #007DFF : #9CA3AF) .padding({ left: 14, right: 14, top: 6, bottom: 6 }) .borderRadius(16) .border({ width: 1, color: this.ac.windSpeed speed ? #007DFF : #334155 }) .onClick(() { if (!this.ac.isOn) return; this.ac.windSpeed speed; }) } ) } .margin({ top: 12 }) // 扫风开关 Row() { Text(上下扫风) Blank() Toggle({ type: ToggleType.Switch, isOn: this.ac.sweepUpDown }) .selectedColor(#007DFF) .onChange((val: boolean) { this.ac.sweepUpDown val; }) } .width(100%) .margin({ top: 16 }) } }注意扫风开关的enabled一样要跟总开关联动。如果空调是关机状态任何子控制都不能动。4.7 辅助功能区辅助功能主要包括定时、睡眠、节能。我用Grid排两列三列的网格按钮图标直接用文字符号代替简单干净。Builder FunctionGrid() { Grid() { GridItem() { this.FunctionButton(定时, this.ac.timerHours 0) } GridItem() { this.FunctionButton(睡眠, this.ac.energySaving) } GridItem() { this.FunctionButton(节能, this.ac.energySaving) } } .columnsTemplate(1fr 1fr 1fr) .columnsGap(12) .rowsGap(12) .margin({ top: 24 }) }定时功能点进去一般会弹一个底部抽屉让用户选择1小时、2小时、4小时、8小时等档位。这个交互用bindSheet实现最合适.bindSheet($$this.showTimerSheet, this.TimerSheetBuilder(), { height: 300, dragBar: true, showClose: true })在TimerSheet里放几个预设值按钮选完直接关闭面板并更新timerHours。睡眠和节能就是单纯toggle这里不再赘述。我这里把“定时”做成按钮弹出面板而“睡眠/节能”做成开关交互上区分了“需要选择具体值”和“单纯开关”两种类型用户体验更合理。5. 多设备适配与细节打磨5.1 窄屏与宽屏布局差异前面提到鸿蒙的响应式能力这里落到代码上。我建议使用GridRow和GridCol来做宽屏断点适配。比如手机宽度下整个页面一列下来平板宽度下温度区和右侧控制区并排。关键代码片段GridRow({ columns: { sm: 1, md: 2, lg: 2 }, gutter: 16 }) { GridCol({ span: { sm: 1, md: 1, lg: 1 } }) { this.TemperatureArea() } GridCol({ span: { sm: 1, md: 1, lg: 1 } }) { Column() { this.ModeSelector() this.WindArea() this.FunctionGrid() } } }这套栅格系统在ArkUI里写起来不复杂关键在于设计阶段就要想好哪些区块可以独立成卡片。我自己的原则是一个区块如果可以在窄屏上单独完整展示那它就有资格在宽屏上独占一列。温度区、模式区、风速区天然满足这个条件所以拆分成两栏非常自然。5.2 深色模式与视觉细节控制页采用深色背景后有几个细节要注意温度数字字号要大52fp以上保证一眼看清。模式按钮的对比度未选中时背景#1E293B选中后#007DFF文字白色。开关的selectedColor设定成品牌色保持一致。页面底部留出Blank()以避开系统导航条。在鸿蒙里颜色用十六进制字符串没问题但更推荐从Resource中引用方便多主题切换。我这篇文章为了演示直观直接写死在代码里实际项目建议把颜色定义到resource的color.json里。5.3 页面转场与数据回传如果你的空调入口是在首页点击某个卡片后跳转到这个控制页那就需要处理参数传递。鸿蒙的router.pushUrl支持带参router.pushUrl({ url: pages/AirControlPage, params: { deviceId: ac_001, deviceName: 客厅空调 } })然后在AirControlPage的aboutToAppear里接收aboutToAppear() { const params router.getParams() as Recordstring, string; if (params) { this.ac.deviceName params[deviceName] ?? 客厅空调; // 如果有设备id可以在这里发起设备状态请求 } }这里有一个小坑router.getParams()返回类型是Object需要用as做类型断言。如果返回空直接解构会报错所以先判空再取值。6. 常见问题与排查心得这部分我把自己开发过程中踩过的一些典型问题列出来你照着对比排查会省不少时间。6.1 温度环进度走满或归零的问题如果温度达到30度Progress的值等于total动画会呈现满环温度调到16度时进度会归零。确实会有人担心“进度为0会不会圆环消失”——实测中Progress的轨道背景依然显示只是颜色弧线为0效果正常。不过要注意Progress的value是浮点类型如果传了 undefined 或者空值控制台会报渲染异常。所以赋值前确保targetTemp已经初始化。我之前遇到过一个问题从首页跳转到控制页时页面先执行build此时ac参数还没初始化温度显示了undefine。解决方案就是在aboutToAppear里第一时间初始化完整数据。6.2 开关状态同步失败很多人用Toggle做开关时只在onChange里更新数据但外部修改数据时组件不刷新。其实Toggle有个isOn属性但你必须确保它是一个状态变量或者把它包在$$双向绑定中Toggle({ type: ToggleType.Switch, isOn: $$this.ac.isOn })用$$绑定后数据与组件状态自动同步省掉手动赋值。这个方法在鸿蒙里叫“双向同步”类似的还有TextInput的text属性也支持$$绑定。如果不用$$就要每次在onChange里手动赋值两边容易漏掉同步。6.3 滑动条拖动时页面卡顿如果你的页面比较大Slider拖动时会频繁触发onChange导致整个页面状态更新可能造成可感知的卡顿。解决办法有两个一是只在SliderChangeMode.End时更新最终值拖动过程中用一个临时值展示二是把Slider所在的子组件独立封装成Component让它的局部状态不触发父组件刷新。我在做温度滑动条时用了第一个方案代码简洁效果稳定.onChange((value: number, mode: SliderChangeMode) { if (mode SliderChangeMode.End) { this.ac.targetTemp Math.round(value); } else { this.tempSliderTemp value; // 局部展示用 } })6.4 键盘弹起遮挡底部内容这个页面没有输入框但如果你扩展了“手动输入温度”的功能就会遇到键盘遮挡的问题。解决方案是给页面外层加expandSafeArea同时监听键盘高度。这个方法在鸿蒙里叫keyboardHeight可以通过window.getLastWindow()获取键盘状态动态调整布局。很多新手忽略这个细节结果桌面测试没问题一到真机上输入温度时按钮全被挡掉了。6.5 按钮点击态不明显如果使用纯Text模拟按钮点击时没有水波纹反馈体验会比较生硬。解决办法把Text包在Button({ type: ButtonType.Normal })里或者用StateStyles定义按压态更偷懒的办法是给Text加onTouch事件在TouchType.Down和Up之间改变透明度。我一般用第一种因为StateStyles是官方推荐的按压效果方案.stateStyles({ pressed: { .backgroundColor(#1E3A5F) } })6.6 页面预览与实际显示不一致DevEco Studio的Previewer有一定的局限性尤其是涉及到router.getParams()这种依赖路由框架的API时可能预览报错。我通常在预览模式里用默认参数兜底真机运行时再走路由参数逻辑。每写一个功能模块先用Previewer看下布局再跑模拟器验证交互。7. 进一步扩展思路这个空调控制页面做扎实之后可以继续往几个方向扩展难度是递增的本地定时器逻辑利用鸿蒙的setTimeout或定时任务实现定时关机并在页面上显示倒计时。设备通信接入通过ohos.net.http或蓝牙ohos.bluetooth.manager对接真实的智能空调设备把AirConditioner类中的数据通过协议下发、设备状态回传后自动同步页面。多设备管理用ForEach渲染一组空调设备每个设备一张卡片点击进入各自的控制页。场景联动把“回家模式”“睡眠模式”做成预设场景一键触发多设备动作。鸿蒙提供了一套跨设备场景联动框架但这里就不展开讲了。我个人在实操作过程中最大的体会是这类控制页面的开发节奏跟普通列表页完全不同。列表页的核心是数据的加载与展示控制页的核心是“状态管理”和“交互反馈”。你写得好不好用户一眼就能感受到——按钮跟不跟手、温度滑不滑、模式切换是否流畅这些全是细节。这篇空调控制页的完整案例代码逻辑并不复杂但覆盖的知识点密度很高。你照着上面的步骤搭一遍再把模式选择、风速控制、辅助功能这些模块都跑通基本上就对ArkTS和ArkUI的主流用法有了系统的掌握。后面再做任何设备控制类页面都会顺很多。最后分享一个实操中的小习惯每次改完一个模块先在模拟器上做一遍完整的“开机—调温—切模式—关风—关机”路径走查再进入下一个模块开发。控制页的状态都是联动的单个模块看着没问题联调时往往会暴露隐藏的状态冲突。早点发现问题后续就能少走很多弯路。
返回列表