HarmonyOS ArkUI 表冠事件(Digital Crown Event)全面解析与实战演示

HarmonyOS ArkUI 表冠事件(Digital Crown Event)全面解析与实战演示

文章目录

一、数字表冠核心概念

1.1 什么是数字表冠?

数字表冠(Digital Crown) 是华为手表侧边的旋转旋钮,类比于鼠标滚轮但专为小屏穿戴设计。它支持两种物理操作:

  1. 旋转(Rotate):顺时针或逆时针拨动,产生连续的增量值,驱动 UI 滚动、数值增减、缩放等
  2. 按压(End/Start):按下/松开表冠,触发确认、返回或阶段性交互

数字表冠的典型应用场景:

  • 列表滚动:在消息列表、联系人列表中上下翻页
  • 数值调节:调节音量、亮度、闹钟时间、运动目标
  • 图片缩放:在图库中放大/缩小预览图片
  • 进度控制:调节视频/音乐播放进度、健康指标设定
  • 菜单选择:在圆形表盘菜单中旋转定位选项

1.2 表冠事件与其他输入事件对比

对比项表冠事件 onDigitalCrown轴事件 onAxisEvent触摸事件 onTouch
输入设备手表数字表冠鼠标滚轮/触控板触摸屏/手写笔
设备限制✅ 仅 Watch 设备PC/平板手机/平板/手表
增量单位sensitivity(原始增量)/ focusSensitivity(框架校准值)offset(XY 偏移量)触点坐标
焦点依赖✅ 需要组件有焦点✅ 需要组件有焦点❌ 无需焦点
旋转方向正值/负值区分顺逆XY 轴方向无方向概念
速度感知focusSensitivity 放大倍率滚动速度滑动速度需手势计算
关键注意onDigitalCrownWatch 专属 API,在手机、平板等非穿戴设备上该回调永远不会触发。开发 Watch 应用时需在 module.json5deviceTypes 中包含 "wearable" 才能正常发布。

二、核心 API 详解

2.1 onDigitalCrown 事件接口

onDigitalCrown 是挂载在任意可聚焦 ArkUI 组件上的通用事件属性,签名如下:

// onDigitalCrown 事件属性签名(ArkTS).onDigitalCrown((event: CrownEvent)=>void)
  • 无返回值:表冠事件不需要消费控制,直接处理回调即可
  • 触发前提:组件必须处于焦点状态,容器组件需显式设置 .focusable(true)
  • 触发时机:用户拨动表冠旋转时持续触发,每次旋转检测间隔约 16ms(60fps 频率)

2.2 CrownEvent 完整结构

CrownEvent 是表冠事件的核心数据对象,包含以下 4 个字段:

// CrownEvent 完整接口说明(ArkTS)interfaceCrownEvent{ timestamp:number// 事件时间戳(毫秒),用于计算旋转速度 deviceId:number// 输入设备 ID,区分不同硬件来源 sensitivity:number// 原始灵敏度增量:传感器检测到的本次旋转量 focusSensitivity:number// 焦点灵敏度增量:框架按焦点强度放大/缩小后的最终值}

2.3 两种灵敏度字段详解

字段类型说明适用场景
sensitivitynumber原始增量,传感器直接输出,正值=顺时针,负值=逆时针需要精确物理感知的场景(如绘图、游戏)
focusSensitivitynumber框架校准值,在原始值基础上乘以焦点权重系数常规 UI 滚动、数值调节(推荐优先使用)
最佳实践:日常开发中优先使用 focusSensitivity 驱动 UI 变化;仅在需要感知硬件原始转速(如转速游戏、精密仪器模拟)时才使用 sensitivity
// 读取两种灵敏度的对比示例.onDigitalCrown((event: CrownEvent)=>{console.info(`时间戳: ${event.timestamp}ms`)console.info(`设备ID: ${event.deviceId}`)console.info(`原始增量 sensitivity: ${event.sensitivity.toFixed(4)}`)console.info(`校准增量 focusSensitivity: ${event.focusSensitivity.toFixed(4)}`)// 正值 = 顺时针旋转;负值 = 逆时针旋转const dir = event.focusSensitivity >0?'顺时针 ↻':'逆时针 ↺'console.info(`旋转方向: ${dir}`)})

三、基础用法:表冠数据实时显示

3.1 可运行完整示例

// entry/src/main/ets/pages/Index.ets@Entry@Component struct BasicCrownDemo {@State timestamp:number=0@State deviceId:number=0@State sensitivity:number=0@State focusSensitivity:number=0@State totalRotation:number=0@State eventCount:number=0@State isFocused:boolean=falsebuild(){Column({ space:12}){Text('表冠事件基础演示').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White).margin({ top:20})// 焦点状态指示Row(){Text(this.isFocused ?'● 监听中':'○ 未激活').fontSize(12).fontColor(this.isFocused ?'#4CAF50':'#9E9E9E')Blank()Text(`触发 ${this.eventCount} 次`).fontSize(12).fontColor('#9E9E9E')}.width('90%')// 数据面板Column({ space:8}){Row(){Text('旋转方向:').fontWeight(FontWeight.Medium).width(120).fontColor('#BBBBBB')}Row(){Text('原始增量:').fontWeight(FontWeight.Medium).width(120).fontColor('#BBBBBB')Text(this.sensitivity.toFixed(4)).fontColor('#42A5F5')}Row(){Text('校准增量:').fontWeight(FontWeight.Medium).width(120).fontColor('#BBBBBB')Text(this.focusSensitivity.toFixed(4)).fontColor('#FF9800')}Row(){Text('累计旋转:').fontWeight(FontWeight.Medium).width(120).fontColor('#BBBBBB')Text(this.totalRotation.toFixed(2)).fontColor('#CE93D8')}Row(){Text('设备 ID:').fontWeight(FontWeight.Medium).width(120).fontColor('#BBBBBB')Text(this.deviceId.toString()).fontColor('#80CBC4')}Row(){Text('时间戳:').fontWeight(FontWeight.Medium).width(120).fontColor('#BBBBBB')Text(this.timestamp.toString()).fontColor('#A5D6A7').fontSize(11)}}.width('90%').padding(14).backgroundColor('#1E1E2E').borderRadius(14).border({ width:1, color:'#333355', style: BorderStyle.Solid }).alignItems(HorizontalAlign.Start)// 表冠感应区Column(){Text(this.isFocused ?'⌚ 请拨动表冠':'👆 点击激活').fontSize(14).fontColor(this.isFocused ?'#4CAF50':'#888888')}.width('80%').height(80).backgroundColor(this.isFocused ?'#1B2A1B':'#1E1E1E').borderRadius(40).justifyContent(FlexAlign.Center).border({ width:2, color:this.isFocused ?'#4CAF50':'#444444', style: BorderStyle.Solid }).focusable(true).animation({ duration:300, curve: Curve.EaseOut }).onFocus(()=>{this.isFocused =true}).onBlur(()=>{this.isFocused =false}).onDigitalCrown((event: CrownEvent)=>{this.timestamp = event.timestamp this.eventCount++})Text('提示:点击圆形区域获取焦点后拨动手表表冠').fontSize(11).fontColor('#666666').textAlign(TextAlign.Center).width('90%')}.width('100%').height('100%').backgroundColor('#121212').alignItems(HorizontalAlign.Center)}}

运行效果如图所示

在这里插入图片描述

四、列表滚动控制

4.1 表冠驱动列表滚动

列表滚动是手表最核心的交互场景。以下示例通过 Scroller 控制器,将 focusSensitivity 增量换算为像素偏移量,实现丝滑的列表滚动效果:

// entry/src/main/ets/pages/Index.ets@Entry@Component struct CrownScrollDemo {@State scrollOffset:number=0@State currentItem:number=0@State crownSpeed:number=0private scroller: Scroller =newScroller()privatereadonlySCROLL_FACTOR:number=80// 灵敏度放大系数private listData:string[]=['🏃 晨跑记录 - 5.2km','💓 心率监测 - 72bpm','😴 睡眠分析 - 7h30m','🔥 卡路里 - 2150kcal','🏋️ 力量训练 - 45min','🚴 骑行记录 - 22km','🧘 冥想训练 - 15min','💧 饮水提醒 - 2.1L','📱 消息通知 - 12条','⏰ 闹钟设置 - 07:30','🌡️ 天气 - 晴 23°C','🎵 音乐控制 - 播放中']build(){Column({ space:0}){// 标题栏Text('健康数据').fontSize(18).fontWeight(FontWeight.Bold).fontColor(Color.White).width('100%').textAlign(TextAlign.Center).padding({ top:16, bottom:12}).backgroundColor('#0A1628')// 速度指示条Row(){Text('旋转速度:').fontSize(11).fontColor('#666666').width(80)Column().height(4).width(`${Math.min(Math.abs(this.crownSpeed)*20,100)}%`).backgroundColor(this.crownSpeed >0?'#4CAF50':'#F44336').borderRadius(2).animation({ duration:100, curve: Curve.Linear })}.width('100%').padding({ left:12, right:12, top:6, bottom:6}).backgroundColor('#0D0D1A')// 可滚动列表List({ scroller:this.scroller, space:2}){ForEach(this.listData,(item:string, index:number)=>{ListItem(){Row(){Text(item).fontSize(14).fontColor(index ===this.currentItem ? Color.White :'#AAAAAA').layoutWeight(1)if(index ===this.currentItem){Text('▶').fontSize(12).fontColor('#4CAF50')}}.width('100%').height(48).padding({ left:16, right:16}).backgroundColor(index ===this.currentItem ?'#1A3A1A':'transparent').borderRadius(8).border({ width: index ===this.currentItem ?1:0, color:'#4CAF50', style: BorderStyle.Solid }).animation({ duration:150, curve: Curve.EaseOut })}})}.layoutWeight(1).width('100%').focusable(true).defaultFocus(true).edgeEffect(EdgeEffect.Spring).onDigitalCrown((event: CrownEvent)=>{// 同步高亮当前可见项const newIndex = Math.max(0, Math.min(this.listData.length -1,))if(newIndex !==this.currentItem){this.currentItem = newIndex }})}.width('100%').height('100%').backgroundColor('#0A0A14')}}

运行效果如图所示

在这里插入图片描述

五、数值调节控制器

5.1 音量/亮度旋钮

手表上调节音量、亮度是典型的"旋钮式"交互。以下示例实现了一个圆形旋钮组件,用表冠驱动数值在 0~100 范围内平滑调节:

// entry/src/main/ets/pages/Index.ets@Entry@Component struct CrownKnobDemo {@State volume:number=50@State brightness:number=75@State activeKnob:number=0// 0=音量, 1=亮度@State isFocused:boolean=falseprivatereadonlySTEP:number=2// 每次增量步进privateclamp(val:number, min:number, max:number):number{return Math.max(min, Math.min(max, val))}@BuilderKnobItem(label:string, value:number, index:number, color:string){Column({ space:6}){// 圆形进度旋钮Stack(){// 背景圆环Column().width(80).height(80).borderRadius(40).backgroundColor('#1A1A2E').border({ width:4, color:'#2A2A3E', style: BorderStyle.Solid })// 进度指示(用 Text 模拟弧形角度)Text(`${value}`).fontSize(22).fontWeight(FontWeight.Bold).fontColor(this.activeKnob === index ? color :'#CCCCCC')}.width(80).height(80).onClick(()=>{this.activeKnob = index }).border({ width:this.activeKnob === index ?2:0, color: color, style: BorderStyle.Solid }).borderRadius(40).animation({ duration:200, curve: Curve.EaseOut })Text(label).fontSize(12).fontColor(this.activeKnob === index ? color :'#666666')// 进度条Column(){Column().width(`${value}%`).height('100%').backgroundColor(color).borderRadius(2).animation({ duration:80, curve: Curve.Linear })}.width(80).height(4).backgroundColor('#2A2A3E').borderRadius(2)}.alignItems(HorizontalAlign.Center)}build(){Column({ space:20}){Text('旋钮调节演示').fontSize(18).fontWeight(FontWeight.Bold).fontColor(Color.White).margin({ top:24})Text(this.isFocused ?'⌚ 拨动表冠调节数值':'👆 点击下方区域激活').fontSize(12).fontColor(this.isFocused ?'#4CAF50':'#666666')// 双旋钮布局Row({ space:30}){this.KnobItem('🔊 音量',this.volume,0,'#42A5F5')this.KnobItem('☀️ 亮度',this.brightness,1,'#FFB300')}// 当前激活旋钮名称Text(`当前调节:${this.activeKnob ===0?'音量':'亮度'} = ${this.activeKnob ===0?this.volume :this.brightness}`).fontSize(14).fontColor(Color.White).fontWeight(FontWeight.Medium)// 快速切换提示Text('点击旋钮切换调节目标,拨动表冠调节数值').fontSize(11).fontColor('#555555').textAlign(TextAlign.Center).width('90%')// 隐藏焦点承载区Column().width('100%').height(60).focusable(true).defaultFocus(true).onFocus(()=>{this.isFocused =true}).onBlur(()=>{this.isFocused =false}).onDigitalCrown((event: CrownEvent)=>{const delta = Math.round(event.focusSensitivity *this.STEP*10)if(this.activeKnob ===0){this.volume =this.clamp(this.volume + delta,0,100)}else{this.brightness =this.clamp(this.brightness + delta,0,100)}})}.width('100%').height('100%').backgroundColor('#0A0A14').alignItems(HorizontalAlign.Center)}}

六、图片缩放控制

6.1 表冠驱动图片缩放

以下示例展示了用表冠控制图片缩放比例,模拟手表图库的放大/缩小交互:

// entry/src/main/ets/pages/Index.ets@Entry@Component struct CrownZoomDemo {@State scale:number=1.0@State scaleText:string='100%'@State isFocused:boolean=false@State lastDelta:number=0privatereadonlyMIN_SCALE:number=0.5privatereadonlyMAX_SCALE:number=3.0privatereadonlyZOOM_FACTOR:number=0.15build(){Column({ space:0}){// 顶部信息栏Row(){Text('图库缩放').fontSize(14).fontColor(Color.White).fontWeight(FontWeight.Medium)Blank()Text(this.scaleText).fontSize(14).fontColor('#42A5F5').fontWeight(FontWeight.Bold)}.width('100%').padding({ left:16, right:16, top:12, bottom:8}).backgroundColor('#0A1020')// 图片展示区(作为焦点承载区)Stack(){// 背景Column().width('100%').height('100%').backgroundColor('#111122')// 图片占位(用渐变色块模拟图片)Column(){Text('🌄').fontSize(60*this.scale).animation({ duration:50, curve: Curve.Linear })Text('风景照片').fontSize(14).fontColor('#AAAAAA').margin({ top:8})}.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).scale({ x:this.scale, y:this.scale }).animation({ duration:50, curve: Curve.Linear })// 焦点激活提示if(!this.isFocused){Column(){Text('👆 点击激活表冠缩放').fontSize(12).fontColor('#888888')}.width('100%').height(40).justifyContent(FlexAlign.Center).backgroundColor('#00000099').position({ x:0, y:'auto'})}}.width('100%').layoutWeight(1).focusable(true).defaultFocus(true).onFocus(()=>{this.isFocused =true}).onBlur(()=>{this.isFocused =false}).onDigitalCrown((event: CrownEvent)=>{this.lastDelta = event.focusSensitivity const newScale =this.scale + event.focusSensitivity *this.ZOOM_FACTORthis.scale = Math.max(this.MIN_SCALE, Math.min(this.MAX_SCALE, newScale))this.scaleText =`${Math.round(this.scale *100)}%`})// 底部控制栏Row({ space:16}){Text('🔍−').fontSize(16).fontColor('#888888')// 缩放进度条Column(){Column().height('100%').width(`${((this.scale -this.MIN_SCALE)/(this.MAX_SCALE-this.MIN_SCALE))*100}%`).backgroundColor('#42A5F5').borderRadius(2).animation({ duration:50, curve: Curve.Linear })}.layoutWeight(1).height(4).backgroundColor('#333333').borderRadius(2)Text('🔍+').fontSize(16).fontColor('#888888')}.width('100%').padding({ left:20, right:20, top:10, bottom:16}).backgroundColor('#0A1020')}.width('100%').height('100%').backgroundColor('#111122')}}

七、进度与时间选择器

7.1 表冠时间轮盘选择器

以下示例实现了手表场景中最常见的时间选择器,通过表冠旋转可分别调节小时和分钟:

// entry/src/main/ets/pages/Index.ets@Entry@Component struct CrownTimePickerDemo {@State hour:number=7@State minute:number=30@State editMode:number=0// 0=调小时, 1=调分钟@State isFocused:boolean=false@State feedbackMsg:string='拨动表冠调节时间'privateformatTime(h:number, m:number):string{return`${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}`}build(){Column({ space:0}){// 时钟显示Column({ space:4}){Text(this.formatTime(this.hour,this.minute)).fontSize(52).fontWeight(FontWeight.Bold).fontColor(Color.White).letterSpacing(4)// 模式切换指示Row({ space:8}){Text('时').fontSize(14).fontColor(this.editMode ===0?'#42A5F5':'#444444').fontWeight(this.editMode ===0? FontWeight.Bold : FontWeight.Normal).padding({ left:12, right:12, top:4, bottom:4}).backgroundColor(this.editMode ===0?'#0D2340':'transparent').borderRadius(8).border({ width:this.editMode ===0?1:0, color:'#42A5F5', style: BorderStyle.Solid }).onClick(()=>{this.editMode =0})Text('分').fontSize(14).fontColor(this.editMode ===1?'#FF9800':'#444444').fontWeight(this.editMode ===1? FontWeight.Bold : FontWeight.Normal).padding({ left:12, right:12, top:4, bottom:4}).backgroundColor(this.editMode ===1?'#2A1A00':'transparent').borderRadius(8).border({ width:this.editMode ===1?1:0, color:'#FF9800', style: BorderStyle.Solid }).onClick(()=>{this.editMode =1})}}.width('100%').padding({ top:40, bottom:30}).backgroundColor('#0A0A14').alignItems(HorizontalAlign.Center)// 反馈信息区Column(){Text(this.isFocused ?'⌚ '+this.feedbackMsg :'👆 点击激活表冠调节').fontSize(13).fontColor(this.isFocused ?'#AAAAAA':'#555555').textAlign(TextAlign.Center)}.width('100%').padding({ top:16, bottom:16}).backgroundColor('#0D0D1A')// 调节区(焦点承载)Column(){Text(this.editMode ===0?'调节小时(0–23)':'调节分钟(0–59)').fontSize(12).fontColor('#555555').margin({ bottom:8})// 当前数值大显示Text(this.editMode ===0?this.hour.toString().padStart(2,'0'):this.minute.toString().padStart(2,'0')).fontSize(48).fontColor(this.editMode ===0?'#42A5F5':'#FF9800').fontWeight(FontWeight.Bold)// 进度条Row(){Column().height(3).width(this.editMode ===0?`${(this.hour /23)*100}%`:`${(this.minute /59)*100}%`).backgroundColor(this.editMode ===0?'#42A5F5':'#FF9800').borderRadius(2).animation({ duration:80, curve: Curve.Linear })}.width('70%').height(3).backgroundColor('#222233').borderRadius(2).margin({ top:10})}.layoutWeight(1).width('100%').backgroundColor('#0A0A14').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).focusable(true).defaultFocus(true).onFocus(()=>{this.isFocused =true}).onBlur(()=>{this.isFocused =false}).onDigitalCrown((event: CrownEvent)=>{const delta = event.focusSensitivity >0?1:-1if(this.editMode ===0){this.hour =(this.hour + delta +24)%24this.feedbackMsg =`小时 → ${this.hour.toString().padStart(2,'0')}`}else{this.minute =(this.minute + delta +60)%60this.feedbackMsg =`分钟 → ${this.minute.toString().padStart(2,'0')}`}})}.width('100%').height('100%').backgroundColor('#0A0A14')}}

八、综合实战:完整表冠交互演示页面

8.1 功能说明

以下综合示例整合了本文所有核心知识点,单个页面通过 Tab 切换同时演示四个场景:

  1. 数据面板:实时显示 CrownEvent 全部 4 个字段及累计统计
  2. 数值调节:双旋钮(音量 + 亮度),点击切换调节目标
  3. 列表滚动:虚拟健康数据列表,表冠滚动 + 高亮跟踪
  4. 时间选择:小时/分钟双模式选择器

8.2 完整可运行代码

// entry/src/main/ets/pages/Index.ets@Entry@Component struct CrownEventDemo {// ── 事件数据 ──────────────────────────────────────@State evtTimestamp:number=0@State evtDeviceId:number=0@State evtSensitivity:number=0@State evtFocusSensitivity:number=0@State evtDirection:string='—'@State evtCount:number=0@State totalRotation:number=0// ── 选项卡 ────────────────────────────────────────@State activeTab:number=0private tabs:string[]=['📊 数据','🎛️ 旋钮','📋 列表','⏰ 时间']// ── 旋钮调节 ──────────────────────────────────────@State volume:number=50@State brightness:number=70@State activeKnob:number=0// ── 列表 ──────────────────────────────────────────@State listHighlight:number=0@State listScrollOffset:number=0private scroller: Scroller =newScroller()private listItems:string[]=['🏃 晨跑 5.2km','💓 心率 72bpm','😴 睡眠 7h30m','🔥 卡路里 2150','🏋️ 训练 45min','🚴 骑行 22km','🧘 冥想 15min','💧 饮水 2.1L','📱 通知 12条','⏰ 闹钟 07:30']// ── 时间选择 ──────────────────────────────────────@State hour:number=8@State minute:number=0@State timeEditMode:number=0// ── 焦点 ──────────────────────────────────────────@State isFocused:boolean=falseprivateclamp(v:number, lo:number, hi:number):number{return Math.max(lo, Math.min(hi, v))}privatehandleCrown(event: CrownEvent):void{// 更新公共数据字段this.evtTimestamp = event.timestamp this.evtDeviceId = event.deviceId this.evtSensitivity = event.sensitivity this.evtFocusSensitivity = event.focusSensitivity this.evtDirection = event.focusSensitivity >0?'顺时针 ↻':'逆时针 ↺'this.evtCount++this.totalRotation += event.focusSensitivity const sign = event.focusSensitivity >0?1:-1switch(this.activeTab){case1:// 旋钮const delta = Math.round(Math.abs(event.focusSensitivity)*20)* sign if(this.activeKnob ===0){this.volume =this.clamp(this.volume + delta,0,100)}else{this.brightness =this.clamp(this.brightness + delta,0,100)}breakcase2:// 列表this.scroller.scrollBy(0, event.focusSensitivity *60)this.listHighlight =this.clamp(this.listHighlight + sign,0,this.listItems.length -1)breakcase3:// 时间if(this.timeEditMode ===0){this.hour =(this.hour + sign +24)%24}else{this.minute =(this.minute + sign +60)%60}break}}@BuilderTabData(){Column({ space:10}){Text(this.isFocused ?'⌚ 拨动表冠观察数据变化':'👆 点击激活').fontSize(11).fontColor(this.isFocused ?'#4CAF50':'#666666').margin({ top:8})Column({ space:8}){Row(){Text('方向:').fontColor('#888888').width(100)Text(this.evtDirection).fontColor(this.evtFocusSensitivity >=0?'#4CAF50':'#F44336').fontWeight(FontWeight.Bold)}Row(){Text('原始增量:').fontColor('#888888').width(100)Text(this.evtSensitivity.toFixed(4)).fontColor('#42A5F5')}Row(){Text('校准增量:').fontColor('#888888').width(100)Text(this.evtFocusSensitivity.toFixed(4)).fontColor('#FF9800')}Row(){Text('累计旋转:').fontColor('#888888').width(100)Text(this.totalRotation.toFixed(2)).fontColor('#CE93D8')}Row(){Text('触发次数:').fontColor('#888888').width(100)Text(this.evtCount.toString()).fontColor('#80CBC4')}Row(){Text('设备 ID:').fontColor('#888888').width(100)Text(this.evtDeviceId.toString()).fontColor('#A5D6A7')}}.width('90%').padding(12).backgroundColor('#1A1A2E').borderRadius(12).border({ width:1, color:'#2A2A3E', style: BorderStyle.Solid }).alignItems(HorizontalAlign.Start)}.width('100%').alignItems(HorizontalAlign.Center)}@BuilderTabKnob(){Column({ space:16}){Text('点击旋钮切换目标,拨动表冠调节').fontSize(11).fontColor('#666666').margin({ top:10})Row({ space:24}){// 音量旋钮Column({ space:6}){Stack(){Column().width(72).height(72).borderRadius(36).backgroundColor('#0D1A2E').border({ width:3, color:this.activeKnob ===0?'#42A5F5':'#222233', style: BorderStyle.Solid })Text(`${this.volume}`).fontSize(20).fontWeight(FontWeight.Bold).fontColor(this.activeKnob ===0?'#42A5F5':'#AAAAAA')}.width(72).height(72).onClick(()=>{this.activeKnob =0})Text('🔊 音量').fontSize(11).fontColor(this.activeKnob ===0?'#42A5F5':'#666666')Column(){Column().height('100%').width(`${this.volume}%`).backgroundColor('#42A5F5').borderRadius(2).animation({ duration:60, curve: Curve.Linear })}.width(72).height(3).backgroundColor('#222233').borderRadius(2)}.alignItems(HorizontalAlign.Center)// 亮度旋钮Column({ space:6}){Stack(){Column().width(72).height(72).borderRadius(36).backgroundColor('#2A1A00').border({ width:3, color:this.activeKnob ===1?'#FF9800':'#222233', style: BorderStyle.Solid })Text(`${this.brightness}`).fontSize(20).fontWeight(FontWeight.Bold).fontColor(this.activeKnob ===1?'#FF9800':'#AAAAAA')}.width(72).height(72).onClick(()=>{this.activeKnob =1})Text('☀️ 亮度').fontSize(11).fontColor(this.activeKnob ===1?'#FF9800':'#666666')Column(){Column().height('100%').width(`${this.brightness}%`).backgroundColor('#FF9800').borderRadius(2).animation({ duration:60, curve: Curve.Linear })}.width(72).height(3).backgroundColor('#222233').borderRadius(2)}.alignItems(HorizontalAlign.Center)}}.width('100%').alignItems(HorizontalAlign.Center)}@BuilderTabList(){Column(){List({ scroller:this.scroller, space:2}){ForEach(this.listItems,(item:string, index:number)=>{ListItem(){Row(){Text(item).fontSize(13).fontColor(index ===this.listHighlight ? Color.White :'#888888').layoutWeight(1)if(index ===this.listHighlight){Text('◀').fontSize(10).fontColor('#4CAF50')}}.width('100%').height(42).padding({ left:14, right:14}).backgroundColor(index ===this.listHighlight ?'#1A3A1A':'transparent').borderRadius(8).animation({ duration:100, curve: Curve.EaseOut })}})}.width('100%').layoutWeight(1).edgeEffect(EdgeEffect.Spring)}.width('100%').layoutWeight(1)}@BuilderTabTime(){Column({ space:12}){Text(`${this.hour.toString().padStart(2,'0')}:${this.minute.toString().padStart(2,'0')}`).fontSize(48).fontWeight(FontWeight.Bold).fontColor(Color.White).margin({ top:20})Row({ space:12}){Text('时').fontSize(14).padding({ left:16, right:16, top:6, bottom:6}).fontColor(this.timeEditMode ===0?'#42A5F5':'#444444').backgroundColor(this.timeEditMode ===0?'#0D2340':'transparent').borderRadius(8).border({ width:this.timeEditMode ===0?1:0, color:'#42A5F5', style: BorderStyle.Solid }).onClick(()=>{this.timeEditMode =0})Text('分').fontSize(14).padding({ left:16, right:16, top:6, bottom:6}).fontColor(this.timeEditMode ===1?'#FF9800':'#444444').backgroundColor(this.timeEditMode ===1?'#2A1A00':'transparent').borderRadius(8).border({ width:this.timeEditMode ===1?1:0, color:'#FF9800', style: BorderStyle.Solid }).onClick(()=>{this.timeEditMode =1})}// 当前编辑值进度条Column(){Column().height('100%').width(this.timeEditMode ===0?`${(this.hour /23)*100}%`:`${(this.minute /59)*100}%`).backgroundColor(this.timeEditMode ===0?'#42A5F5':'#FF9800').borderRadius(2).animation({ duration:80, curve: Curve.Linear })}.width('70%').height(4).backgroundColor('#222233').borderRadius(2)Text(this.timeEditMode ===0?'调节小时 (0–23)':'调节分钟 (0–59)').fontSize(11).fontColor('#555555')}.width('100%').alignItems(HorizontalAlign.Center)}build(){Column({ space:0}){// 顶部标题Text('ArkUI 表冠事件综合演示').fontSize(17).fontWeight(FontWeight.Bold).fontColor(Color.White).width('100%').textAlign(TextAlign.Center).padding({ top:14, bottom:10}).backgroundColor('#0A1020')// 选项卡Row(){ForEach(this.tabs,(tab:string, index:number)=>{Text(tab).fontSize(11).layoutWeight(1).textAlign(TextAlign.Center).fontColor(this.activeTab === index ?'#42A5F5':'#666666').fontWeight(this.activeTab === index ? FontWeight.Bold : FontWeight.Normal).padding({ top:8, bottom:8}).border({ width:{ bottom:this.activeTab === index ?2:0}, color:'#42A5F5', style: BorderStyle.Solid }).onClick(()=>{this.activeTab = index })})}.width('100%').backgroundColor('#0D1020').border({ width:{ bottom:1}, color:'#1A1A2E', style: BorderStyle.Solid })// 主内容区(含 focusable + onDigitalCrown)Column({ space:0}){if(this.activeTab ===0){this.TabData()}elseif(this.activeTab ===1){this.TabKnob()}elseif(this.activeTab ===2){this.TabList()}else{this.TabTime()}}.layoutWeight(1).width('100%').focusable(true).defaultFocus(true).onFocus(()=>{this.isFocused =true}).onBlur(()=>{this.isFocused =false}).onDigitalCrown((event: CrownEvent)=>{this.handleCrown(event)})}.width('100%').height('100%').backgroundColor('#0A0A14')}}

九、注意事项与最佳实践

9.1 设备兼容性

表冠事件是 Watch 专属能力,开发时必须注意:

  1. deviceTypes 配置module.json5 中需包含 "wearable" 才能使用表冠相关 API
  2. 模拟器选择:在 DevEco Studio 的 Device Manager 中选择 Watch 类型模拟器运行
  3. 非手表设备onDigitalCrown 回调在手机、平板上不会触发,应提供触摸/按键兜底交互
  4. 硬件差异:不同型号手表表冠的 sensitivity 基准值可能不同,建议基于 focusSensitivity 做相对控制
// module.json5 设备类型配置{"module":{"deviceTypes":["wearable"],// 必须包含 wearable...}}

9.2 灵敏度调参建议

focusSensitivity 是框架归一化后的值,直接使用时应乘以业务系数:

场景推荐系数说明
列表滚动(像素)× 80~120每档旋转滚动约 80~120px
音量/亮度(0~100)× 10~20每档旋转变化约 2~4%
图片缩放(倍率)× 0.1~0.2每档旋转缩放约 10~20%
时间选择(档)取符号 sign每档旋转 ±1
游戏精确控制使用 sensitivity 原始值需要精确物理感知

9.3 焦点与事件生命周期

// 标准的焦点 + 表冠事件联动模式Column().focusable(true).defaultFocus(true)// 页面入口组件设置默认焦点.onFocus(()=>{// 获得焦点时启动动画/状态提示this.isListening =true}).onBlur(()=>{// 失去焦点时停止反馈、重置状态this.isListening =false}).onDigitalCrown((event: CrownEvent)=>{// 在此处理旋转逻辑// 无需返回值,直接处理即可})
常见误区ColumnRowStack 等容器组件默认 focusable=false,必须显式添加 .focusable(true) 后才能响应表冠事件。同时推荐在页面根容器加 .defaultFocus(true) 避免进入页面后表冠无响应。

9.4 性能优化建议

表冠旋转会以约 60fps 的频率持续触发事件,处理不当会导致 UI 卡顿:

  • 避免在回调中做耗时计算:复杂算法应使用节流(throttle)或防抖(debounce)处理
  • 状态更新精简化:每次只更新必要的 @State 变量,减少无效重渲染
  • 动画时长设短:配套动画使用 50~100ms 短时长,保证跟手感
  • Scroller.scrollBy 异步化:大列表滚动考虑使用 animateTo 包裹,避免同步阻塞

十、与其他事件协同使用

10.1 表冠 + 触摸双通道

在手表 UI 中,同一功能应同时支持表冠和触摸操作:

// 表冠 + 触摸操作同一 @State@State selectedIndex:number=0Column().focusable(true).onDigitalCrown((event: CrownEvent)=>{// 表冠旋转选择const sign = event.focusSensitivity >0?1:-1this.selectedIndex = Math.max(0, Math.min(9,this.selectedIndex + sign))})// 同一列表项也支持触摸点击Text(item).onClick(()=>{// 触摸点击直接跳转this.selectedIndex = index })

10.2 表冠 + 按键联动

手表侧边按键(onKeyEvent)与表冠配合,实现"旋转选择、按键确认"的标准手表交互模式:

Column().focusable(true).onDigitalCrown((event: CrownEvent)=>{// 表冠旋转:移动光标const sign = event.focusSensitivity >0?1:-1this.cursorIndex =(this.cursorIndex + sign +this.items.length)%this.items.length }).onKeyEvent((event: KeyEvent)=>{// 侧键按下:确认选择if(event.type === KeyType.Down && event.keyCode === KeyCode.KEYCODE_DPAD_CENTER){this.confirmedItem =this.items[this.cursorIndex]returntrue}returnfalse})
最佳实践:表冠负责「导航/调节」,侧键(物理按键)负责「确认/返回」,两者职责分离,符合手表 UX 设计规范。避免将确认操作放在表冠的旋转停止判断中,因为 CrownEvent 没有明确的 End 阶段信号。

总结

本文系统讲解了 HarmonyOS ArkUI 表冠事件 的完整知识体系,核心要点回顾:

  1. CrownEvent 四字段timestamp(时间戳)、deviceId(设备ID)、sensitivity(原始增量)、focusSensitivity(框架校准值)
  2. 两种灵敏度sensitivity 用于精密物理感知,focusSensitivity 用于常规 UI 驱动(推荐)
  3. 焦点是前提:容器组件必须设置 .focusable(true),入口组件建议加 .defaultFocus(true)
  4. Watch 专属onDigitalCrown 仅在 wearable 设备类型上生效,需配置 module.json5
  5. 四大场景:列表滚动(系数 80~120)、旋钮调节(系数 10~20)、图片缩放(系数 0.1~0.2)、时间选择(取符号)
  6. 协同使用:表冠负责导航/调节,侧键(onKeyEvent)负责确认,触摸作为兜底
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Read more

python迭代器和生成器

1.迭代器 1.1 可迭代对象         可迭代对象指的是 实现了__iter__方法的对象,可以被for循环遍历的容器,比如一个列表,自定义的链表等。使用 iter() 方法获取它的迭代对象(可以理解为链表的指针) 1.2 迭代器         迭代器是指实现了 __iter__ 和 __next__方法对象,__iter__返回它本身,确保每个单独的节点都是可以被迭代的,满足链表可以从第n个元素开始访问的需求;__next__返回它的下一个节点,如果已经遍历完毕则抛出StopIteration异常。 # 实现了__iter__方法,是一个可迭代对象,可以理解为是一个整的链表,但此时还不能单独访问其中的元素 class Node: def __init__(self, data): self.data = data self.next: Node | None

By Ne0inhk
初始Python篇(8)—— 异常

初始Python篇(8)—— 异常

找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程(ಥ_ಥ)-ZEEKLOG博客 所属专栏: Python 目录 异常介绍 异常的处理 try-except  try-except-else  try-except-else-finally 异常的抛出  常见的异常类型   异常介绍 在学习异常之前,先了解bug的概念。简单理解就是程序运行不起来或者运行起来之后,输出的结果不符合我们的预期。有了bug肯定是要去解决的。而解决bug就离不开异常的处理。 如果是程序运行出错,有两种情况:1、程序本身就存在问题;2、对于存在输入型程序来说,可能是用户的输入导致程序报错。 第一种情况,就需要我们自己去排查,看是哪里出现了语法错误。 第二种情况,就需要用到我们今天学习的异常处理。 如果是程序运行之后,结果不符合我们的预期,这就需要用到调试工具了。 异常的处理 try-except  Python中对于异常的处理是通过 try-except 语句来捕获异常的。 语法: try: ... // 可能出现异常的代码 except 异常

By Ne0inhk
Python操作国产金仓数据库(KingbaseES)全流程:从环境搭建到实战应用

Python操作国产金仓数据库(KingbaseES)全流程:从环境搭建到实战应用

Python操作国产金仓数据库(KingbaseES)全流程:从环境搭建到实战应用 Python操作国产金仓数据库(KingbaseES)全流程:从环境搭建到实战应用,大家好,我是 xcLeigh。现在国产化数据库越来越普及,金仓数据库(KingbaseES)作为一款超实用的企业级关系型数据库,在政府、金融、能源这些关键领域用得特别多。今天我就带大家从零开始,一步步学会用Python操作金仓数据库,从环境准备、连接数据库,到CRUD核心操作、事务处理,再到常见问题排查,内容全是干货,代码拿过去就能用,就算是新手也能很快上手! 前言     中电科金仓(北京)科技股份有限公司(以下简称“电科金仓”)成立于1999年,是成立最早的拥有自主知识产权的国产数据库企业,也是中国电子科技集团(CETC)成员企业。电科金仓以“提供卓越的数据库产品助力企业级应用高质量发展”为使命,致力于“成为世界卓越的数据库产品与服务提供商”。     电科金仓自成立起始终坚持自主创新,专注数据库领域二十余载,具备出色的数据库产品研发及服务能力,核心产品金仓数据库管理系统KingbaseES(简称“KES”

By Ne0inhk
使用 Python 脚本一键上传图片到兰空图床并自动复制链接

使用 Python 脚本一键上传图片到兰空图床并自动复制链接

个人博客:材料与逻辑 对于博客作者、开发者或任何经常需要处理图片的人来说,图床是绕不开的工具。兰空图床(Lsky Pro)凭借其强大的功能和现代化的界面,成为了许多自建图床用户的首选。 但是,传统的“打开浏览器 -> 登录 -> 拖拽上传 -> 点击复制链接”的流程在需要频繁插入图片时显得格外繁琐,严重打断写作或开发的思路。 今天,我将分享一个简单的 Python 脚本,它能彻底改变你的图片上传体验。 核心功能:快,准,狠 这个脚本旨在实现一个目标:以最快速度将本地图片转换为可用的网络链接。 它具备以下核心特性: 1. 完全脱离浏览器:直接在终端(命令行)通过一条命令完成上传。 2. 基于文件名传参:无需复杂的配置,只需告诉脚本你要传哪个文件。 3. 自动复制到剪切板:这是灵魂功能!上传成功后,图片

By Ne0inhk