1# 焦点事件 2 3## 基础概念与规范 4 5### 基础概念 6 7**焦点、焦点链和走焦** 8 9- 焦点:指向当前应用界面上唯一的一个可交互元素,当用户使用键盘、电视遥控器、车机摇杆/旋钮等非指向性输入设备与应用程序进行间接交互时,基于焦点的导航和交互是重要的输入手段。 10- 焦点链:在应用的组件树形结构中,当一个组件获得焦点时,从根节点到该组件节点的整条路径上的所有节点都会被视为处于焦点状态,形成一条连续的焦点链。 11- 走焦:指焦点在应用内的组件之间转移的行为。这一过程对用户是透明的,但开发者可以通过监听onFocus(焦点获取)和onBlur(焦点失去)事件来捕捉这些变化。关于走焦的具体方式和规则,详见[走焦规范](#走焦规范)。 12 13 14**焦点态** 15 16用来指向当前获焦组件的样式。 17 18- 显示规则:默认情况下焦点态不会显示,只有当应用进入激活态后,焦点态才会显示。因此,虽然获得焦点的组件不一定显示焦点态(取决于是否处于激活态),但显示焦点态的组件必然是获得焦点的。大部分组件内置了焦点态样式,开发者同样可以使用样式接口进行自定义,一旦自定义,组件将不再显示内置的焦点态样式。在焦点链中,若多个组件同时拥有焦点态,系统将采用子组件优先的策略,优先显示子组件的焦点态,并且仅显示一个焦点态。 19- 进入激活态:使用外接键盘按下TAB键/使用FocusController的activate(true)方法才会进入焦点的激活态,进入激活态后,才可以使用键盘TAB键/方向键进行走焦。首次用来激活焦点态的TAB键不会触发走焦。 20- 退出激活态:当应用收到FocusController的active(false)方法/点击事件时(包括手指触屏的按下事件和鼠标左键的按下事件),焦点的激活态会退出。 21 22 23**层级页面** 24 25层级页面是焦点框架中特定容器组件的统称,涵盖Page、Dialog、SheetPage、ModalPage、Menu、Popup、NavBar、NavDestination等。这些组件通常具有以下关键特性: 26 27- 视觉层级独立性:从视觉呈现上看,这些组件独立于其他页面内容,并通常位于其上方,形成视觉上的层级差异。 28- 焦点跟随:此类组件在首次创建并展示之后,会立即将应用内焦点抢占。 29- 走焦范围限制:当焦点位于这些组件内部时,用户无法通过键盘按键将焦点转移到组件外部的其他元素上,焦点移动仅限于组件内部。 30 31在一个应用程序中,任何时候都至少存在一个层级页面组件,并且该组件会持有当前焦点。当该层级页面关闭或不再可见时,焦点会自动转移到下一个可用的层级页面组件上,确保用户交互的连贯性和一致性。 32 33> **说明:** 34> 35> Popup组件在focusable属性(组件属性,非通用属性)为false的时候,不会有第2条特性。 36> 37> NavBar、NavDestination没有第3条特性,对于它们的走焦范围,是与它们的首个父层级页面相同的。 38 39**根容器** 40 41根容器是层级页面内的概念,当某个层级页面首次创建并展示时,根据层级页面的特性,焦点会立即被该页面抢占。此时,该层级页面所在焦点链的末端节点将成为默认焦点,而这个默认焦点通常位于该层级页面的根容器上。 42 43在缺省状态下,层级页面的默认焦点位于其根容器上,但开发者可以通过defaultFocus属性来自定义这一行为。 44 45当焦点位于根容器时,首次按下TAB键不仅会使焦点进入激活状态,还会触发焦点向子组件的传递。如果子组件本身也是一个容器,则焦点会继续向下传递,直至到达叶子节点。传递规则是:优先传递给上一次获得焦点的子节点,如果不存在这样的节点,则默认传递给第一个子节点。 46 47### 走焦规范 48 49根据走焦的触发方式,可以分为主动走焦和被动走焦。 50 51**主动走焦** 52 53 54指开发者/用户主观行为导致的焦点移动,包括:使用外接键盘的按键走焦(TAB键/Shift+TAB键/方向键)、使用requestFocus申请焦点、clearFocus清除焦点、focusOnTouch点击申请焦点等接口导致的焦点转移。 55 56 57- 按键走焦 581. 前提:当前应用需处于焦点激活态。 592. 范围限制:按键走焦仅在当前获得焦点的层级页面内进行,具体参见“层级页面”中的“走焦范围限制”部分。 603. 按键类型: 61TAB键:遵循Z字型遍历逻辑,完成当前范围内所有叶子节点的遍历,到达当前范围内的最后一个组件后,继续按下TAB键,焦点将循环至范围内的第一个可获焦组件,实现循环走焦。 62Shift+TAB键:与TAB键具有相反的焦点转移效果。 63方向键(上、下、左、右):遵循十字型移动策略,在单层容器中,焦点的转移由该容器的特定走焦算法决定。若算法判定下一个焦点应落在某个容器组件上,系统将采用中心点距离优先的算法来进一步确定容器内的目标子节点。 644. 走焦算法:每个可获焦的容器组件都有其特定的走焦算法,用于定义焦点转移的规则。 655. 子组件优先:当子组件处理按键走焦事件,父组件将不再介入。 66 67- requestFocus 68详见[主动获焦失焦](#主动获焦失焦),可以主动将焦点转移到指定组件上。 69不可跨窗口,不可跨ArkUI实例申请焦点,可以跨层级页面申请焦点。 70 71- clearFocus 72详见[clearFocus](../reference/apis-arkui/js-apis-arkui-UIContext.md#clearfocus12),会清除当前层级页面中的焦点,最终焦点停留在根容器上。 73 74- focusOnTouch 75详见[focusOnTouch](../reference/apis-arkui/arkui-ts/ts-universal-attributes-focus.md#focusontouch9),使绑定组件具备点击后获得焦点的能力。若组件本身不可获焦,则此功能无效。若绑定的是容器组件,点击后优先将焦点转移给上一次获焦的子组件,否则转移给第一个可获焦的子组件。 76 77 78**被动走焦** 79 80被动走焦是指组件焦点因系统或其他操作而自动转移,无需开发者直接干预,这是焦点系统的默认行为。 81 82 83目前会被动走焦的机制有: 84 85- 组件删除:当处于焦点状态的组件被删除时,焦点框架首先尝试将焦点转移到相邻的兄弟组件上,遵循先向后再向前的顺序。若所有兄弟组件均不可获焦,则焦点将释放,并通知其父组件进行焦点处理。 86- 属性变更:若将处于焦点状态的组件的focusable或enabled属性设置为false,或者将visibility属性设置为不可见,系统将自动转移焦点至其他可获焦组件,转移方式与1中相同。 87- 层级页面切换:当发生层级页面切换时,如从一个页面跳转到另一个页面,当前页面的焦点将自动释放,新页面可能会根据预设逻辑自动获得焦点。 88- Web组件初始化:对于Web组件,当其被创建时,若其设计需要立即获得焦点(如某些弹出框或输入框),则可能触发焦点转移至该Web组件,其行为属于组件自身的行为逻辑,不属于焦点框架的规格范围。 89 90### 走焦算法 91 92在焦点管理系统中,每个可获焦的容器都配备有特定的走焦算法,这些算法定义了当使用TAB键、Shift+TAB键或方向键时,焦点如何从当前获焦的子组件转移到下一个可获焦的子组件。 93 94容器采用何种走焦算法取决于其UX(用户体验)规格,并由容器组件进行适配。目前,焦点框架支持三种走焦算法:线性走焦、投影走焦和自定义走焦。 95 96**线性走焦算法** 97 98 99线性走焦算法是默认的走焦策略,它基于容器中子节点在节点树中的挂载顺序进行走焦,常用于单方向布局的容器,如Row、Column和Flex容器。运行规则如下: 100 101 102- 顺序依赖:走焦顺序完全基于子节点在节点树中的挂载顺序,与它们在界面上的实际布局位置无关。 103- TAB键走焦:使用TAB键时,焦点将按照子节点的挂载顺序依次遍历。 104- 方向键走焦:当使用与容器定义方向垂直的方向键时,容器不接受该方向的走焦请求。例如,在横向的Row容器中,无法使用方向键进行上下移动。 105- 边界处理:当焦点位于容器的首尾子节点时,容器将拒绝与当前焦点方向相反的方向键走焦请求。例如,焦点在一个横向的Row容器的第一个子节点上时,该容器无法处理方向键左的走焦请求。 106 107 108**投影走焦算法** 109 110投影走焦算法基于当前获焦组件在走焦方向上的投影,结合子组件与投影的重叠面积和中心点距离进行胜出判定。该算法特别适用于子组件大小不一的容器,目前仅有配置了wrap属性的Flex组件。运行规则如下: 111 112 113- 方向键走焦时,判断投影与子组件区域的重叠面积,在所有面积不为0的子组件中,计算它们与当前获焦组件的中心点直线距离,距离最短的胜出,若存在多个备选,则节点树上更靠前的胜出。若无任何子组件与投影由重叠,说明该容器已经无法处理该方向键的走焦请求。 114- TAB键走焦时,先使用规格1,按照方向键右进行判定,若找到则成功退出,若无法找到,则将当前获焦子组件的位置模拟往下移动该获焦子组件的高度,然后再按照方向键左进行投影判定,有投影重叠且中心点直线距离最远的子组件胜出,若无投影重叠的子组件,则表示该容器无法处理本次TAB键走焦请求。 115- Shift+TAB键走焦时,先使用规格1,按照方向键左进行判定,找到则成功退出。若无法找到,则将当前获焦子组件的位置模拟向上移动该获焦子组件的高度,然后再按照方向键右进行投影判定,有投影重叠且中心点直线距离最远的子组件胜出,若无投影重叠的子组件,则表示该容器无法处理本次的Shift+TAB键走焦请求。 116 117 118**自定义走焦算法** 119 120由组件自定义的走焦算法,规格由组件定义。 121 122## 获焦/失焦事件 123 124```ts 125onFocus(event: () => void) 126``` 127 128 129获焦事件回调,绑定该接口的组件获焦时,回调响应。 130 131```ts 132onBlur(event:() => void) 133``` 134 135失焦事件回调,绑定该接口的组件失焦时,回调响应。 136 137onFocus和onBlur两个接口通常成对使用,来监听组件的焦点变化。 138 139```ts 140// xxx.ets 141@Entry 142@Component 143struct FocusEventExample { 144 @State oneButtonColor: Color = Color.Gray; 145 @State twoButtonColor: Color = Color.Gray; 146 @State threeButtonColor: Color = Color.Gray; 147 148 build() { 149 Column({ space: 20 }) { 150 // 通过外接键盘的上下键可以让焦点在三个按钮间移动,按钮获焦时颜色变化,失焦时变回原背景色 151 Button('First Button') 152 .width(260) 153 .height(70) 154 .backgroundColor(this.oneButtonColor) 155 .fontColor(Color.Black) 156 // 监听第一个组件的获焦事件,获焦后改变颜色 157 .onFocus(() => { 158 this.oneButtonColor = Color.Green; 159 }) 160 // 监听第一个组件的失焦事件,失焦后改变颜色 161 .onBlur(() => { 162 this.oneButtonColor = Color.Gray; 163 }) 164 165 Button('Second Button') 166 .width(260) 167 .height(70) 168 .backgroundColor(this.twoButtonColor) 169 .fontColor(Color.Black) 170 // 监听第二个组件的获焦事件,获焦后改变颜色 171 .onFocus(() => { 172 this.twoButtonColor = Color.Green; 173 }) 174 // 监听第二个组件的失焦事件,失焦后改变颜色 175 .onBlur(() => { 176 this.twoButtonColor = Color.Grey; 177 }) 178 179 Button('Third Button') 180 .width(260) 181 .height(70) 182 .backgroundColor(this.threeButtonColor) 183 .fontColor(Color.Black) 184 // 监听第三个组件的获焦事件,获焦后改变颜色 185 .onFocus(() => { 186 this.threeButtonColor = Color.Green; 187 }) 188 // 监听第三个组件的失焦事件,失焦后改变颜色 189 .onBlur(() => { 190 this.threeButtonColor = Color.Gray ; 191 }) 192 }.width('100%').margin({ top: 20 }) 193 } 194} 195``` 196 197 198 199 200 201上述示例包含以下3步: 202 203- 应用打开,按下TAB键激活走焦,“First Button”显示焦点态样式:组件外围有一个蓝色的闭合框,onFocus回调响应,背景色变成绿色。 204- 按下TAB键,触发走焦,“Second Button”获焦,onFocus回调响应,背景色变成绿色;“First Button”失焦,onBlur回调响应,背景色变回灰色。 205- 按下TAB键,触发走焦,“Third Button”获焦,onFocus回调响应,背景色变成绿色;“Second Button”失焦,onBlur回调响应,背景色变回灰色。 206 207## 设置组件是否可获焦 208 209```ts 210focusable(value: boolean) 211``` 212 213设置组件是否可获焦。 214 215按照组件的获焦能力可大致分为三类: 216 217- 默认可获焦的组件,通常是有交互行为的组件,例如Button、Checkbox、TextInput组件,此类组件无需设置任何属性,默认即可获焦。 218 219- 有获焦能力,但默认不可获焦的组件,典型的是Text、Image组件,此类组件缺省情况下无法获焦,若需要使其获焦,可使用通用属性focusable(true)使能。对于没有配置focusable属性,有获焦能力但默认不可获焦的组件,为其配置onClick或是单指单击的Tap手势,该组件会隐式地成为可获焦组件。如果其focusable属性被设置为false,即使配置了上述事件,该组件依然不可获焦。 220 221- 无获焦能力的组件,通常是无任何交互行为的展示类组件,例如Blank、Circle组件,此类组件即使使用focusable属性也无法使其可获焦。 222 223 224```ts 225enabled(value: boolean) 226``` 227 228设置组件可交互性属性[enabled](../reference/apis-arkui/arkui-ts/ts-universal-attributes-enable.md#enabled)为`false`,则组件不可交互,无法获焦。 229 230 231```ts 232visibility(value: Visibility) 233``` 234 235设置组件可见性属性[visibility](../reference/apis-arkui/arkui-ts/ts-universal-attributes-visibility.md#visibility)为`Visibility.None`或`Visibility.Hidden`,则组件不可见,无法获焦。 236 237 238```ts 239focusOnTouch(value: boolean) 240``` 241 242设置当前组件是否支持点击获焦能力。 243 244 245>**说明:** 246> 247>当某组件处于获焦状态时,将其的focusable属性或enabled属性设置为false,会自动使该组件失焦,然后焦点按照[走焦规范](#走焦规范)将焦点转移给其他组件。 248 249 250```ts 251// xxx.ets 252@Entry 253@Component 254struct FocusableExample { 255 @State textFocusable: boolean = true; 256 @State textEnabled: boolean = true; 257 @State color1: Color = Color.Yellow; 258 @State color2: Color = Color.Yellow; 259 @State color3: Color = Color.Yellow; 260 261 build() { 262 Column({ space: 5 }) { 263 Text('Default Text') // 第一个Text组件未设置focusable属性,默认不可获焦 264 .borderColor(this.color1) 265 .borderWidth(2) 266 .width(300) 267 .height(70) 268 .onFocus(() => { 269 this.color1 = Color.Blue; 270 }) 271 .onBlur(() => { 272 this.color1 = Color.Yellow; 273 }) 274 Divider() 275 276 Text('focusable: ' + this.textFocusable) // 第二个Text设置了focusable初始为true,focusableOnTouch为true 277 .borderColor(this.color2) 278 .borderWidth(2) 279 .width(300) 280 .height(70) 281 .focusable(this.textFocusable) 282 .focusOnTouch(true) 283 .onFocus(() => { 284 this.color2 = Color.Blue; 285 }) 286 .onBlur(() => { 287 this.color2 = Color.Yellow; 288 }) 289 290 Text('enabled: ' + this.textEnabled) // 第三个Text设置了focusable为true,enabled初始为true 291 .borderColor(this.color3) 292 .borderWidth(2) 293 .width(300) 294 .height(70) 295 .focusable(true) 296 .enabled(this.textEnabled) 297 .focusOnTouch(true) 298 .onFocus(() => { 299 this.color3 = Color.Blue; 300 }) 301 .onBlur(() => { 302 this.color3 = Color.Yellow; 303 }) 304 305 Divider() 306 307 Row() { 308 Button('Button1') 309 .width(140).height(70) 310 Button('Button2') 311 .width(160).height(70) 312 } 313 314 Divider() 315 Button('Button3') 316 .width(300).height(70) 317 318 Divider() 319 }.width('100%').justifyContent(FlexAlign.Center) 320 .onKeyEvent((e) => { 321 // 绑定onKeyEvent,在该Column组件获焦时,按下'F'键,可将第二个Text的focusable置反 322 if (e.keyCode === 2022 && e.type === KeyType.Down) { 323 this.textFocusable = !this.textFocusable; 324 } 325 // 绑定onKeyEvent,在该Column组件获焦时,按下'G'键,可将第三个Text的enabled置反 326 if (e.keyCode === 2023 && e.type === KeyType.Down) { 327 this.textEnabled = !this.textEnabled; 328 } 329 }) 330 } 331} 332``` 333 334 335运行效果: 336 337 338 339 340上述示例包含以下3步: 341 342 343- 第一个Text组件没有设置focusable(true)属性,该Text组件无法获焦。 344- 点击第二个Text组件,由于设置了focusOnTouch(true),第二个组件获焦。按下TAB键,触发走焦,仍然是第二个Text组件获焦。按键盘F键,触发onKeyEvent,focusable置为false,第二个Text组件变成不可获焦,焦点自动转移,会自动从Text组件寻找下一个可获焦组件,焦点转移到第三个Text组件上。 345- 按键盘G键,触发onKeyEvent,enabled置为false,第三个Text组件变成不可获焦,焦点自动转移,使焦点转移到Row容器上,容器中使用的是默认配置,会转移到Button1上。 346 347## 默认焦点 348 349### 页面的默认焦点 350 351```ts 352defaultFocus(value: boolean) 353``` 354 355设置当前组件是否为当前页面上的默认焦点。 356 357 358```ts 359// xxx.ets 360@Entry 361@Component 362struct morenjiaodian { 363 @State oneButtonColor: Color = Color.Gray; 364 @State twoButtonColor: Color = Color.Gray; 365 @State threeButtonColor: Color = Color.Gray; 366 367 build() { 368 Column({ space: 20 }) { 369 // 通过外接键盘的上下键可以让焦点在三个按钮间移动,按钮获焦时颜色变化,失焦时变回原背景色 370 Button('First Button') 371 .width(260) 372 .height(70) 373 .backgroundColor(this.oneButtonColor) 374 .fontColor(Color.Black) 375 // 监听第一个组件的获焦事件,获焦后改变颜色 376 .onFocus(() => { 377 this.oneButtonColor = Color.Green; 378 }) 379 // 监听第一个组件的失焦事件,失焦后改变颜色 380 .onBlur(() => { 381 this.oneButtonColor = Color.Gray; 382 }) 383 384 Button('Second Button') 385 .width(260) 386 .height(70) 387 .backgroundColor(this.twoButtonColor) 388 .fontColor(Color.Black) 389 // 监听第二个组件的获焦事件,获焦后改变颜色 390 .onFocus(() => { 391 this.twoButtonColor = Color.Green; 392 }) 393 // 监听第二个组件的失焦事件,失焦后改变颜色 394 .onBlur(() => { 395 this.twoButtonColor = Color.Grey; 396 }) 397 398 Button('Third Button') 399 .width(260) 400 .height(70) 401 .backgroundColor(this.threeButtonColor) 402 .fontColor(Color.Black) 403 // 设置默认焦点 404 .defaultFocus(true) 405 // 监听第三个组件的获焦事件,获焦后改变颜色 406 .onFocus(() => { 407 this.threeButtonColor = Color.Green; 408 }) 409 // 监听第三个组件的失焦事件,失焦后改变颜色 410 .onBlur(() => { 411 this.threeButtonColor = Color.Gray ; 412 }) 413 }.width('100%').margin({ top: 20 }) 414 } 415} 416``` 417 418 419 420上述示例包含以下2步: 421 422- 在第三个Button组件上设置了defaultFocus(true),进入页面后第三个Button默认获焦,显示为绿色。 423- 按下TAB键,触发走焦,第三个Button正处于获焦状态,会出现焦点框。 424 425### 容器的默认焦点 426 427容器的默认焦点受到[获焦优先级](#焦点组与获焦优先级)的影响。 428 429**defaultFocus与FocusPriority的区别** 430 431[defaultFocus](../reference/apis-arkui/arkui-ts/ts-universal-attributes-focus.md#defaultfocus9)是用于指定页面首次展示时的默认获焦节点,[FocusPriority](../reference/apis-arkui/arkui-ts/ts-universal-attributes-focus.md#focuspriority12)是用于指定某个容器首次获焦时其子节点的获焦优先级。上述两个属性在某些场景同时配置时行为未定义,例如下面的场景,页面首次展示无法同时满足defaultFocus获焦和高优先级组件获焦。 432 433示例 434 435```ts 436@Entry 437@Component 438struct Index { 439 build() { 440 Row() { 441 Button('Button1') 442 .defaultFocus(true) 443 Button('Button2') 444 .focusScopePriority('RowScope', FocusPriority.PREVIOUS) 445 }.focusScopeId('RowScope') 446 } 447} 448``` 449 450### 页面/容器整体获焦时的焦点链 451 452**整体获焦与非整体获焦** 453 454- 整体获焦是页面/容器自身作为焦点链的叶节点获焦,获焦后再把焦点链叶节点转移到子孙组件。例如,页面切换、Navigation组件中的路由切换、焦点组走焦、容器组件主动调用requestFocusById等。 455 456- 非整体获焦是某个组件作为焦点链叶节点获焦,导致其祖先节点跟着获焦。例如TextInput组件主动获取焦点、Tab键在非焦点组场景下走焦等。 457 458**整体获焦的焦点链形成** 459 4601.页面首次获焦: 461 462- 焦点链叶节点为配置了defaultFocus的节点。 463 464- 未配置defaultFocus时,焦点停留在页面的根容器上。 465 4662.页面非首次获焦:由上次获焦的节点获焦。 467 4683.获焦链上存在配置了获焦优先级的组件和容器: 469 470- 容器内存在优先级大于PREVIOUS的组件,由优先级最高的组件获焦。 471 472- 容器内不存在优先级大于PREVIOUS的组件,由上次获焦的节点获焦。例如,窗口失焦后重新获焦。 473 474 475## 焦点样式 476 477 478```ts 479focusBox(style: FocusBoxStyle) 480``` 481 482设置当前组件系统焦点框样式。 483 484```ts 485import { ColorMetrics, LengthMetrics } from '@kit.ArkUI' 486 487@Entry 488@Component 489struct RequestFocusExample { 490 build() { 491 Column({ space: 30 }) { 492 Button("small black focus box") 493 .focusBox({ 494 margin: new LengthMetrics(0), 495 strokeColor: ColorMetrics.rgba(0, 0, 0), 496 }) 497 Button("large red focus box") 498 .focusBox({ 499 margin: LengthMetrics.px(20), 500 strokeColor: ColorMetrics.rgba(255, 0, 0), 501 strokeWidth: LengthMetrics.px(10) 502 }) 503 } 504 .alignItems(HorizontalAlign.Center) 505 .width('100%') 506 } 507} 508``` 509 510 511 512 513上述示例包含以下2步: 514 515- 进入页面,按下TAB触发走焦,第一个Button获焦,焦点框样式为紧贴边缘的蓝色细框。 516- 按下TAB键,走焦到第二个Button,焦点框样式为远离边缘的红色粗框。 517 518## 主动获焦/失焦 519 520- 使用FocusController中的方法 521 522 更推荐使用FocusController中的requestFocus主动获取焦点。优势如下: 523 - 当前帧生效,避免被下一帧组件树变化影响。 524 - 有异常值返回,便于排查主动获取焦点失败的原因。 525 - 避免多实例场景中取到错误实例。 526 527 需先使用UIContext中的[getFocusController()](../reference/apis-arkui/js-apis-arkui-UIContext.md#getfocuscontroller12)方法获取实例,再通过此实例调用对应方法。 528 529 ```ts 530 requestFocus(key: string): void 531 ``` 532 通过组件的id将焦点转移到组件树对应的实体节点,生效时间为当帧生效。 533 534 ```ts 535 clearFocus(): void 536 ``` 537 清除焦点,将焦点强制转移到页面根容器节点,焦点链路上其他节点失焦。 538 539- 使用focusControl中的方法 540 ```ts 541 requestFocus(value: string): boolean 542 ``` 543 544 调用此接口可以主动让焦点转移至参数指定的组件上,焦点转移生效时间为下一个帧信号。 545 546 547```ts 548// focusTest.ets 549@Entry 550@Component 551struct RequestExample { 552 @State btColor: string = '#ff2787d9' 553 @State btColor2: string = '#ff2787d9' 554 555 build() { 556 Column({ space: 20 }) { 557 Column({ space: 5 }) { 558 Button('Button') 559 .width(200) 560 .height(70) 561 .fontColor(Color.White) 562 .focusOnTouch(true) 563 .backgroundColor(this.btColor) 564 .onFocus(() => { 565 this.btColor = '#ffd5d5d5' 566 }) 567 .onBlur(() => { 568 this.btColor = '#ff2787d9' 569 }) 570 .id("testButton") 571 572 Button('Button') 573 .width(200) 574 .height(70) 575 .fontColor(Color.White) 576 .focusOnTouch(true) 577 .backgroundColor(this.btColor2) 578 .onFocus(() => { 579 this.btColor2 = '#ffd5d5d5' 580 }) 581 .onBlur(() => { 582 this.btColor2 = '#ff2787d9' 583 }) 584 .id("testButton2") 585 586 Divider() 587 .vertical(false) 588 .width("80%") 589 .backgroundColor('#ff707070') 590 .height(10) 591 592 Button('FocusController.requestFocus') 593 .width(200).height(70).fontColor(Color.White) 594 .onClick(() => { 595 this.getUIContext().getFocusController().requestFocus("testButton") 596 }) 597 .backgroundColor('#ff2787d9') 598 599 Button("focusControl.requestFocus") 600 .width(200).height(70).fontColor(Color.White) 601 .onClick(() => { 602 focusControl.requestFocus("testButton2") 603 }) 604 .backgroundColor('#ff2787d9') 605 606 Button("clearFocus") 607 .width(200).height(70).fontColor(Color.White) 608 .onClick(() => { 609 this.getUIContext().getFocusController().clearFocus() 610 }) 611 .backgroundColor('#ff2787d9') 612 } 613 } 614 .width('100%') 615 .height('100%') 616 } 617} 618``` 619 620 621 622上述示例包含以下3步: 623 624- 点击FocusController.requestFocus按钮,第一个Button获焦。 625- 点击focusControl.requestFocus按钮,第二个Button获焦。 626- 点击clearFocus按钮,第二个Button失焦。 627 628## 焦点组与获焦优先级 629 630```ts 631focusScopePriority(scopeId: string, priority?: FocusPriority) 632``` 633 634设置当前组件在指定容器内获焦的优先级。需要配合focusScopeId一起使用。 635 636 637```ts 638focusScopeId(id: string, isGroup?: boolean) 639``` 640 641设置当前容器组件的id标识,设置当前容器组件是否为焦点组。焦点组与tabIndex不能混用。 642 643```ts 644// focusTest.ets 645@Entry 646@Component 647struct FocusableExample { 648 @State inputValue: string = '' 649 650 build() { 651 Scroll() { 652 Row({ space: 20 }) { 653 Column({ space: 20 }) { // 标记为Column1 654 Column({ space: 5 }) { 655 Button('Group1') 656 .width(165) 657 .height(40) 658 .fontColor(Color.White) 659 Row({ space: 5 }) { 660 Button() 661 .width(80) 662 .height(40) 663 .fontColor(Color.White) 664 Button() 665 .width(80) 666 .height(40) 667 .fontColor(Color.White) 668 } 669 Row({ space: 5 }) { 670 Button() 671 .width(80) 672 .height(40) 673 .fontColor(Color.White) 674 Button() 675 .width(80) 676 .height(40) 677 .fontColor(Color.White) 678 } 679 }.borderWidth(2).borderColor(Color.Red).borderStyle(BorderStyle.Dashed) 680 Column({ space: 5 }) { 681 Button('Group2') 682 .width(165) 683 .height(40) 684 .fontColor(Color.White) 685 Row({ space: 5 }) { 686 Button() 687 .width(80) 688 .height(40) 689 .fontColor(Color.White) 690 Button() 691 .width(80) 692 .height(40) 693 .fontColor(Color.White) 694 .focusScopePriority('ColumnScope1', FocusPriority.PRIOR) // Column1首次获焦时获焦 695 } 696 Row({ space: 5 }) { 697 Button() 698 .width(80) 699 .height(40) 700 .fontColor(Color.White) 701 Button() 702 .width(80) 703 .height(40) 704 .fontColor(Color.White) 705 } 706 }.borderWidth(2).borderColor(Color.Green).borderStyle(BorderStyle.Dashed) 707 } 708 .focusScopeId('ColumnScope1') 709 Column({ space: 5 }) { // 标记为Column2 710 TextInput({placeholder: 'input', text: this.inputValue}) 711 .onChange((value: string) => { 712 this.inputValue = value 713 }) 714 .width(156) 715 Button('Group3') 716 .width(165) 717 .height(40) 718 .fontColor(Color.White) 719 Row({ space: 5 }) { 720 Button() 721 .width(80) 722 .height(40) 723 .fontColor(Color.White) 724 Button() 725 .width(80) 726 .height(40) 727 .fontColor(Color.White) 728 } 729 Button() 730 .width(165) 731 .height(40) 732 .fontColor(Color.White) 733 .focusScopePriority('ColumnScope2', FocusPriority.PREVIOUS) // Column2获焦时获焦 734 Row({ space: 5 }) { 735 Button() 736 .width(80) 737 .height(40) 738 .fontColor(Color.White) 739 Button() 740 .width(80) 741 .height(40) 742 .fontColor(Color.White) 743 } 744 Button() 745 .width(165) 746 .height(40) 747 .fontColor(Color.White) 748 Row({ space: 5 }) { 749 Button() 750 .width(80) 751 .height(40) 752 .fontColor(Color.White) 753 Button() 754 .width(80) 755 .height(40) 756 .fontColor(Color.White) 757 } 758 }.borderWidth(2).borderColor(Color.Orange).borderStyle(BorderStyle.Dashed) 759 .focusScopeId('ColumnScope2', true) // Column2为焦点组 760 }.alignItems(VerticalAlign.Top) 761 } 762 } 763} 764``` 765 766 767 768 769 770 771上述示例包含以下2步: 772 773- input方框内设置了焦点组,因此按下TAB键后焦点会快速从input中走出去,而按下方向键后可以在input内走焦。 774- 左上角的Column没有设置焦点组,因此只能通过Tab键一个一个地走焦。 775 776## 焦点与按键事件 777 778当组件获焦且存在点击事件(`onClick`)或单指单击事件(`TapGesture`)时,回车和空格会触发对应的事件回调。 779 780> **说明:** 781> 782> 1. 点击事件(`onClick`)或单指单击事件(`TapGesture`)在回车、空格触发对应事件回调时,默认不冒泡传递,即父组件对应[按键事件](../reference/apis-arkui/arkui-ts/ts-universal-events-key.md)不会被同步触发。 783> 2. 按键事件(`onKeyEvent`)默认冒泡传递,即同时会触发父组件的按键事件回调。 784> 3. 组件同时存在点击事件(`onClick`)和按键事件(`onKeyEvent`),在回车、空格触发时,两者都会响应。 785> 4. 获焦组件响应点击事件(`onClick`),与焦点激活态无关。 786 787``` 788@Entry 789@Component 790struct FocusOnclickExample { 791 @State count: number = 0 792 @State name: string = 'Button' 793 794 build() { 795 Column() { 796 Button(this.name) 797 .fontSize(30) 798 .onClick(() => { 799 this.count++ 800 if (this.count <= 0) { 801 this.name = "count is negative number" 802 } else if (this.count % 2 === 0) { 803 this.name = "count is even number" 804 } else { 805 this.name = "count is odd number" 806 } 807 }).height(60) 808 }.height('100%').width('100%').justifyContent(FlexAlign.Center) 809 } 810} 811``` 812 813 814## 组件获焦能力说明 815 816 817 **表1** 基础组件获焦能力 818 819| 基础组件 | 是否有获焦能力 | focusable默认值 | 820| ---------------------------------------- | ------- | ------------ | 821| [AlphabetIndexer](../reference/apis-arkui/arkui-ts/ts-container-alphabet-indexer.md) | 是 | true | 822| [Blank](../reference/apis-arkui/arkui-ts/ts-basic-components-blank.md) | 否 | false | 823| [Button](../reference/apis-arkui/arkui-ts/ts-basic-components-button.md) | 是 | true | 824| [CalendarPicker](../reference/apis-arkui/arkui-ts/ts-basic-components-calendarpicker.md) | 是 | true | 825| [Checkbox](../reference/apis-arkui/arkui-ts/ts-basic-components-checkbox.md) | 是 | true | 826| [CheckboxGroup](../reference/apis-arkui/arkui-ts/ts-basic-components-checkboxgroup.md) | 是 | true | 827| [ContainerSpan](../reference/apis-arkui/arkui-ts/ts-basic-components-containerspan.md) | 否 | false | 828| [DataPanel](../reference/apis-arkui/arkui-ts/ts-basic-components-datapanel.md) | 是 | false | 829| [DatePicker](../reference/apis-arkui/arkui-ts/ts-basic-components-datepicker.md) | 是 | true | 830| [Divider](../reference/apis-arkui/arkui-ts/ts-basic-components-divider.md) | 是 | false | 831| [Gauge](../reference/apis-arkui/arkui-ts/ts-basic-components-gauge.md) | 是 | false | 832| [Image](../reference/apis-arkui/arkui-ts/ts-basic-components-image.md) | 是 | false | 833| [ImageAnimator](../reference/apis-arkui/arkui-ts/ts-basic-components-imageanimator.md) | 否 | false | 834| [ImageSpan](../reference/apis-arkui/arkui-ts/ts-basic-components-imagespan.md) | 否 | false | 835| [LoadingProgress](../reference/apis-arkui/arkui-ts/ts-basic-components-loadingprogress.md) | 是 | true | 836| [Marquee](../reference/apis-arkui/arkui-ts/ts-basic-components-marquee.md) | 否 | false | 837| [Menu](../reference/apis-arkui/arkui-ts/ts-basic-components-menu.md) | 是 | true | 838| [MenuItem](../reference/apis-arkui/arkui-ts/ts-basic-components-menuitem.md) | 是 | true | 839| [MenuItemGroup](../reference/apis-arkui/arkui-ts/ts-basic-components-menuitemgroup.md) | 否 | false | 840| [Navigation](../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md) | 是 | true | 841| [NavRouter](../reference/apis-arkui/arkui-ts/ts-basic-components-navrouter.md) | 否 | false | 842| [NavDestination](../reference/apis-arkui/arkui-ts/ts-basic-components-navdestination.md) | 是 | true | 843| [PatternLock](../reference/apis-arkui/arkui-ts/ts-basic-components-patternlock.md) | 是 | true | 844| [Progress](../reference/apis-arkui/arkui-ts/ts-basic-components-progress.md) | 是 | true | 845| [QRCode](../reference/apis-arkui/arkui-ts/ts-basic-components-qrcode.md) | 是 | true | 846| [Radio](../reference/apis-arkui/arkui-ts/ts-basic-components-radio.md) | 是 | true | 847| [Rating](../reference/apis-arkui/arkui-ts/ts-basic-components-rating.md) | 是 | true | 848| [RichEditor](../reference/apis-arkui/arkui-ts/ts-basic-components-richeditor.md) | 是 | true | 849| [RichText](../reference/apis-arkui/arkui-ts/ts-basic-components-richtext.md) | 否 | false | 850| [ScrollBar](../reference/apis-arkui/arkui-ts/ts-basic-components-scrollbar.md) | 否 | false | 851| [Search](../reference/apis-arkui/arkui-ts/ts-basic-components-search.md) | 是 | true | 852| [Select](../reference/apis-arkui/arkui-ts/ts-basic-components-select.md) | 是 | true | 853| [Slider](../reference/apis-arkui/arkui-ts/ts-basic-components-slider.md) | 是 | true | 854| [Span](../reference/apis-arkui/arkui-ts/ts-basic-components-span.md) | 否 | false | 855| [Stepper](../reference/apis-arkui/arkui-ts/ts-basic-components-stepper.md) | 是 | true | 856| [StepperItem](../reference/apis-arkui/arkui-ts/ts-basic-components-stepperitem.md) | 是 | true | 857| [SymbolSpan](../reference/apis-arkui/arkui-ts/ts-basic-components-symbolSpan.md) | 否 | false | 858| [SymbolGlyph](../reference/apis-arkui/arkui-ts/ts-basic-components-symbolGlyph.md) | 否 | false | 859| [Text](../reference/apis-arkui/arkui-ts/ts-basic-components-text.md) | 是 | false | 860| [TextArea](../reference/apis-arkui/arkui-ts/ts-basic-components-textarea.md) | 否 | false | 861| [TextClock](../reference/apis-arkui/arkui-ts/ts-basic-components-textclock.md) | 否 | false | 862| [TextInput](../reference/apis-arkui/arkui-ts/ts-basic-components-textinput.md) | 是 | true | 863| [TextPicker](../reference/apis-arkui/arkui-ts/ts-basic-components-textpicker.md) | 是 | true | 864| [TextTimer](../reference/apis-arkui/arkui-ts/ts-basic-components-texttimer.md) | 否 | false | 865| [TimePicker](../reference/apis-arkui/arkui-ts/ts-basic-components-timepicker.md) | 否 | false | 866| [Toggle](../reference/apis-arkui/arkui-ts/ts-basic-components-toggle.md) | 是 | true | 867| [XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md) | 是 | false | 868 869 **表2** 容器组件获焦能力 870 871| 容器组件 | 是否可获焦 | focusable默认值 | 872| ---------------------------------------- | ----- | ------------ | 873| [Badge](../reference/apis-arkui/arkui-ts/ts-container-badge.md) | 否 | false | 874| [Column](../reference/apis-arkui/arkui-ts/ts-container-column.md) | 是 | true | 875| [ColumnSplit](../reference/apis-arkui/arkui-ts/ts-container-columnsplit.md) | 是 | true | 876| [Counter](../reference/apis-arkui/arkui-ts/ts-container-counter.md) | 是 | false | 877| [EmbeddedComponent](../reference/apis-arkui/arkui-ts/ts-container-embedded-component.md) | 否 | false | 878| [Flex](../reference/apis-arkui/arkui-ts/ts-container-flex.md) | 是 | true | 879| [FlowItem](../reference/apis-arkui/arkui-ts/ts-container-flowitem.md) | 是 | true | 880| [FolderStack](../reference/apis-arkui/arkui-ts/ts-container-folderstack.md) | 是 | true | 881| [FormLink](../reference/apis-arkui/arkui-ts/ts-container-formlink.md) | 否 | false | 882| [GridCol](../reference/apis-arkui/arkui-ts/ts-container-gridcol.md) | 是 | true | 883| [GridRow](../reference/apis-arkui/arkui-ts/ts-container-gridrow.md) | 是 | true | 884| [Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md) | 是 | true | 885| [GridItem](../reference/apis-arkui/arkui-ts/ts-container-griditem.md) | 是 | true | 886| [Hyperlink](../reference/apis-arkui/arkui-ts/ts-container-hyperlink.md) | 是 | true | 887| [List](../reference/apis-arkui/arkui-ts/ts-container-list.md) | 是 | true | 888| [ListItem](../reference/apis-arkui/arkui-ts/ts-container-listitem.md) | 是 | true | 889| [ListItemGroup](../reference/apis-arkui/arkui-ts/ts-container-listitemgroup.md) | 是 | true | 890| [Navigator](../reference/apis-arkui/arkui-ts/ts-container-navigator.md) | 是 | true | 891| [Refresh](../reference/apis-arkui/arkui-ts/ts-container-refresh.md) | 是 | true | 892| [RelativeContainer](../reference/apis-arkui/arkui-ts/ts-container-relativecontainer.md) | 否 | false | 893| [Row](../reference/apis-arkui/arkui-ts/ts-container-row.md) | 是 | true | 894| [RowSplit](../reference/apis-arkui/arkui-ts/ts-container-rowsplit.md) | 是 | true | 895| [Scroll](../reference/apis-arkui/arkui-ts/ts-container-scroll.md) | 是 | true | 896| [SideBarContainer](../reference/apis-arkui/arkui-ts/ts-container-sidebarcontainer.md) | 是 | true | 897| [Stack](../reference/apis-arkui/arkui-ts/ts-container-stack.md) | 是 | true | 898| [Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md) | 是 | true | 899| [Tabs](../reference/apis-arkui/arkui-ts/ts-container-tabs.md) | 是 | true | 900| [TabContent](../reference/apis-arkui/arkui-ts/ts-container-tabcontent.md) | 是 | true | 901| [WaterFlow](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md) | 否 | false | 902| [WithTheme](../reference/apis-arkui/arkui-ts/ts-container-with-theme.md) | 是 | true | 903 904 **表3** 媒体组件获焦能力 905 906| 媒体组件 | 是否可获焦 | focusable默认值 | 907| ---------------------------------------- | ----- | ------------ | 908| [Video](../reference/apis-arkui/arkui-ts/ts-media-components-video.md) | 是 | true |