1# 焦点事件 2 3 4## 基本概念 5 6- 焦点 7 8 指向当前应用界面上唯一的一个可交互元素,当用户使用键盘、电视遥控器、车机摇杆/旋钮等非指向性输入设备与应用程序进行间接交互时,基于焦点的导航和交互是重要的输入手段。 9 10- 默认焦点 11 12 应用打开或切换页面后,若当前页上存在可获焦的组件,则树形结构的组件树中第一个可获焦的组件默认获得焦点。可以使用[自定义默认焦点](#自定义默认焦点)进行自定义指定。 13 14- 获焦 15 16 指组件获得了焦点,同一时刻,应用中最多只有1个末端组件是获焦的,且此时它的所有祖宗组件(整个组件链)均是获焦的。当期望某个组件获焦,须确保该组件及其所有的祖宗节点均是可获焦的([focusable](#设置组件是否获焦)属性为true)。 17 18- 失焦 19 20 指组件从获焦状态变成了非获焦状态,失去了焦点。组件失焦时,它的所有祖宗组件(失焦组件链)与新的获焦组件链不相同的节点都会失焦。 21 22- 走焦 23 24 表示焦点在当前应用中转移的过程,走焦会带来原焦点组件的失焦和新焦点组件的获焦。应用中焦点发生变化的方式按行为可分为两类: 25 26 - 主动走焦:指开发者/用户主观的行为导致焦点移动,包含:外接键盘上按下TAB/方向键、使用[requestFocus](#focuscontrolrequestfocus)主动给指定组件申请焦点、组件[focusOnTouch](#focusontouch)属性为true后点击组件。 27 - 被动走焦:指组件焦点因其他操作被动的转移焦点,此特性为焦点系统默认行为,无法由开发者自由设定,例如当使用if-else语句将处于获焦的组件删除/将处于获焦的组件(或其父组件)置成不可获焦时、当页面切换时。 28 29- 焦点态 30 31 获焦组件的样式,不同组件的焦点态样式大同小异,默认情况下焦点态不显示,仅使用外接键盘按下TAB键/方向键时才会触发焦点态样式出现。首次触发焦点态显示的TAB键/方向键不会触发走焦。当应用接收到点击事件时(包括手指触屏的按下事件和鼠标左键的按下事件),自动隐藏焦点态样式。焦点态样式由后端组件定义,开发者无法修改。 32 33 34## 走焦规则 35 36走焦规则是指用户使用“TAB键/SHIFT+TAB键/方向键”主动进行走焦,或焦点系统在执行被动走焦时的顺序规则。组件的走焦规则默认由走焦系统定义,由焦点所在的容器决定。 37 38- 线性走焦:常见的容器有Flex、Row、Column、List,这些都是典型的单方向容器,组件在这些容器内的排列都是线性的,那么走焦规则也是线性的。走焦的方向和方向键的方向一致。 39 40 **图1** 线性走焦示意图 41 42 ![zh-cn_image_0000001562700537](figures/zh-cn_image_0000001562700537.png) 43 44 例如Row容器,使用方向键左右(←/→)即可将焦点在相邻的2个可获焦组件之间来回切换。 45 46- 十字走焦:使用方向键上(↑)下(↓)左(←)右(→)可以使焦点在相邻的组件上切换。典型的是Grid容器,如下图: 47 48 **图2** Grid组件十字走焦示意图 49 50 ![zh-cn_image_0000001511740580](figures/zh-cn_image_0000001511740580.png) 51 52 >**说明:** 53 > 54 > - TAB/SHIFT+TAB键在以上两种走焦规则上的功能和方向键一致。TAB键等同于“先执行方向键右,若无法走焦,再执行方向键下”,SHIFT+TAB键等同于“先执行方向键左,若无法走焦,再执行方向键上”。 55 > 56 > - 触发走焦的按键是按下的事件(DOWN事件)。 57 > 58 > - 删除组件、设置组件无法获焦后,会使用线性走焦规则,自动先往被删除/Unfocusable组件的前置兄弟组件上走焦,无法走焦的话,再忘后置兄弟组件上走焦。 59 60- tabIndex走焦:给组件设置[tabIndex](../reference/arkui-ts/ts-universal-attributes-focus.md)通用属性,自定义组件的TAB键/SHIFT+TAB键的走焦顺序。 61 62- 区域走焦:给容器组件设置tabIndex通用属性,再结合[groupDefaultFocus](#groupdefaultfocus)通用属性,自定义容器区域的TAB键/SHIFT+TAB键的走焦顺序和默认获焦组件。 63 64- 走焦至容器组件规则:当焦点走焦到容器(该容器没有配置groupDefaultFocus)上时,若该容器组件为首次获焦,则会先计算目标容器组件的子组件的区域位置,得到距离目标容器中心点最近的子组件,焦点会走到目标容器上的该子组件上。若该容器非首次获焦,焦点会自动走焦到上一次目标容器中获焦的子组件。 65 66- 焦点交互:当某组件获焦时,该组件的固有点击任务或开发者绑定的onClick回调任务,会自动挂载到空格/回车按键上,当按下按键时,任务就和手指/鼠标点击一样被执行。 67 68 69>**说明:** 70> 71>本文涉及到的焦点均为组件焦点,另外一个焦点的概念是:窗口焦点,指向当前获焦的窗口。当窗口失焦时,该窗口应用中的所有获焦组件全部失焦。 72 73 74## 监听组件的焦点变化 75 76 77```ts 78onFocus(event: () => void) 79``` 80 81 82获焦事件回调,绑定该API的组件获焦时,回调响应。 83 84 85 86```ts 87onBlur(event:() => void) 88``` 89 90 91失焦事件回调,绑定该API的组件失焦时,回调响应。 92 93 94onFocus和onBlur两个接口通常成对使用,来监听组件的焦点变化。 95 96 97以下示例代码展示获焦/失焦回调的使用方法: 98 99 100 101```ts 102// xxx.ets 103@Entry 104@Component 105struct FocusEventExample { 106 @State oneButtonColor: Color = Color.Gray; 107 @State twoButtonColor: Color = Color.Gray; 108 @State threeButtonColor: Color = Color.Gray; 109 110 build() { 111 Column({ space: 20 }) { 112 // 通过外接键盘的上下键可以让焦点在三个按钮间移动,按钮获焦时颜色变化,失焦时变回原背景色 113 Button('First Button') 114 .width(260) 115 .height(70) 116 .backgroundColor(this.oneButtonColor) 117 .fontColor(Color.Black) 118 // 监听第一个组件的获焦事件,获焦后改变颜色 119 .onFocus(() => { 120 this.oneButtonColor = Color.Green; 121 }) 122 // 监听第一个组件的失焦事件,失焦后改变颜色 123 .onBlur(() => { 124 this.oneButtonColor = Color.Gray; 125 }) 126 127 Button('Second Button') 128 .width(260) 129 .height(70) 130 .backgroundColor(this.twoButtonColor) 131 .fontColor(Color.Black) 132 // 监听第二个组件的获焦事件,获焦后改变颜色 133 .onFocus(() => { 134 this.twoButtonColor = Color.Green; 135 }) 136 // 监听第二个组件的失焦事件,失焦后改变颜色 137 .onBlur(() => { 138 this.twoButtonColor = Color.Grey; 139 }) 140 141 Button('Third Button') 142 .width(260) 143 .height(70) 144 .backgroundColor(this.threeButtonColor) 145 .fontColor(Color.Black) 146 // 监听第三个组件的获焦事件,获焦后改变颜色 147 .onFocus(() => { 148 this.threeButtonColor = Color.Green; 149 }) 150 // 监听第三个组件的失焦事件,失焦后改变颜色 151 .onBlur(() => { 152 this.threeButtonColor = Color.Gray ; 153 }) 154 }.width('100%').margin({ top: 20 }) 155 } 156} 157``` 158 159 160![zh-cn_image_0000001511740584](figures/zh-cn_image_0000001511740584.gif) 161 162 163上述示例包含以下4步: 164 165 1661. 应用打开时,“First Button”默认获取焦点,onFocus回调响应,背景色变成绿色。 167 1682. 按下TAB键(或方向键下↓),“First Button”显示焦点态样式:组件外围有一个蓝色的闭合框。不触发走焦,焦点仍然在“First Button”上。 169 1703. 按下TAB键(或方向键下↓),触发走焦,“Second Button”获焦,onFocus回调响应,背景色变成绿色;“First Button”失焦、onBlur回调响应,背景色变回灰色。 171 1724. 按下TAB键(或方向键下↓),触发走焦,“Third Button”获焦,onFocus回调响应,背景色变成绿色;“Second Button”失焦、onBlur回调响应,背景色变回灰色。 173 174 175## 设置组件是否获焦 176 177通过focusable接口设置组件是否可获焦: 178 179 180```ts 181focusable(value: boolean) 182``` 183 184按照组件的获焦能力可大致分为三类: 185 186- 默认可获焦的组件,通常是有交互行为的组件,例如Button、Checkbox,TextInput组件,此类组件无需设置任何属性,默认即可获焦。 187 188- 有获焦能力,但默认不可获焦的组件,典型的是Text、Image组件,此类组件缺省情况下无法获焦,若需要使其获焦,可使用通用属性focusable(true)使能。 189 190- 无获焦能力的组件,通常是无任何交互行为的展示类组件,例如Blank、Circle组件,此类组件即使使用focusable属性也无法使其可获焦。 191 192 193>**说明:** 194> - focusable为false表示组件不可获焦,同样可以使组件变成不可获焦的还有通用属性[enabled](../reference/arkui-ts/ts-universal-attributes-enable.md)。 195> 196> - 当某组件处于获焦状态时,将其的focusable属性或enabled属性设置为false,会自动使该组件失焦,然后焦点按照[走焦规则](#走焦规则)将焦点转移给其他组件。 197 198 **表1** 基础组件获焦能力 199 200| 基础组件 | 是否有获焦能力 | focusable默认值 | 走焦规则 | 201| ---------------------------------------- | ------- | ------------ | -------- | 202| [AlphabetIndexer](../reference/arkui-ts/ts-container-alphabet-indexer.md) | 是 | true | 线性走焦 | 203| [Blank](../reference/arkui-ts/ts-basic-components-blank.md) | 否 | false | / | 204| [Button](../reference/arkui-ts/ts-basic-components-button.md) | 是 | true | / | 205| [Checkbox](../reference/arkui-ts/ts-basic-components-checkbox.md) | 是 | true | / | 206| [CheckboxGroup](../reference/arkui-ts/ts-basic-components-checkboxgroup.md) | 是 | true | / | 207| [DataPanel](../reference/arkui-ts/ts-basic-components-datapanel.md) | 否 | false | / | 208| [DatePicker](../reference/arkui-ts/ts-basic-components-datepicker.md) | 是 | true | 线性走焦 | 209| [Divider](../reference/arkui-ts/ts-basic-components-divider.md) | 否 | false | / | 210| [Formcomponent](../reference/arkui-ts/ts-basic-components-formcomponent.md) | 否 | false | / | 211| [Gauge](../reference/arkui-ts/ts-basic-components-gauge.md) | 否 | false | / | 212| [Image](../reference/arkui-ts/ts-basic-components-image.md) | 是 | false | / | 213| [ImageAnimator](../reference/arkui-ts/ts-basic-components-imageanimator.md) | 是 | false | / | 214| [LoadingProgress](../reference/arkui-ts/ts-basic-components-loadingprogress.md) | 否 | false | / | 215| [Marquee](../reference/arkui-ts/ts-basic-components-marquee.md) | 否 | false | / | 216| [Menu](../reference/arkui-ts/ts-basic-components-menu.md) | 是 | true | 线性走焦 | 217| [MenuItem](../reference/arkui-ts/ts-basic-components-menuitem.md) | 是 | true | / | 218| [MenuItemGroup](../reference/arkui-ts/ts-basic-components-menuitemgroup.md) | 是 | true | 线性走焦 | 219| [Navigation](../reference/arkui-ts/ts-basic-components-navigation.md) | 否 | false | 组件自定义 | 220| [NavRouter](../reference/arkui-ts/ts-basic-components-navrouter.md) | 否 | false | 跟随子容器 | 221| [NavDestination](../reference/arkui-ts/ts-basic-components-navdestination.md) | 否 | false | 线性走焦 | 222| [PatternLock](../reference/arkui-ts/ts-basic-components-patternlock.md) | 否 | false | / | 223| [PluginComponent](../reference/arkui-ts/ts-basic-components-plugincomponent.md) | 否 | false | / | 224| [Progress](../reference/arkui-ts/ts-basic-components-progress.md) | 否 | false | / | 225| [QRCode](../reference/arkui-ts/ts-basic-components-qrcode.md) | 否 | false | / | 226| [Radio](../reference/arkui-ts/ts-basic-components-radio.md) | 是 | true | / | 227| [Rating](../reference/arkui-ts/ts-basic-components-rating.md) | 是 | true | / | 228| [RemoteWindow](../reference/arkui-ts/ts-basic-components-remotewindow.md) | 否 | false | / | 229| [RichText](../reference/arkui-ts/ts-basic-components-richtext.md) | 否 | false | / | 230| [ScrollBar](../reference/arkui-ts/ts-basic-components-scrollbar.md) | 否 | false | / | 231| [Search](../reference/arkui-ts/ts-basic-components-search.md) | 是 | true | / | 232| [Select](../reference/arkui-ts/ts-basic-components-select.md) | 是 | true | 线性走焦 | 233| [Slider](../reference/arkui-ts/ts-basic-components-slider.md) | 是 | true | / | 234| [Span](../reference/arkui-ts/ts-basic-components-span.md) | 否 | false | / | 235| [Stepper](../reference/arkui-ts/ts-basic-components-stepper.md) | 是 | true | / | 236| [StepperItem](../reference/arkui-ts/ts-basic-components-stepperitem.md) | 是 | true | / | 237| [Text](../reference/arkui-ts/ts-basic-components-text.md) | 是 | false | / | 238| [TextArea](../reference/arkui-ts/ts-basic-components-textarea.md) | 是 | true | / | 239| [TextClock](../reference/arkui-ts/ts-basic-components-textclock.md) | 否 | false | / | 240| [TextInput](../reference/arkui-ts/ts-basic-components-textinput.md) | 是 | true | / | 241| [TextPicker](../reference/arkui-ts/ts-basic-components-textpicker.md) | 是 | true | 线性走焦 | 242| [TextTimer](../reference/arkui-ts/ts-basic-components-texttimer.md) | 否 | false | / | 243| [TimePicker](../reference/arkui-ts/ts-basic-components-timepicker.md) | 是 | true | 线性走焦 | 244| [Toggle](../reference/arkui-ts/ts-basic-components-toggle.md) | 是 | true | / | 245| [Web](../reference/arkui-ts/ts-basic-components-web.md) | 是 | true | Web组件自定义 | 246| [XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md) | 否 | false | / | 247 248 **表2** 容器组件获焦能力 249 250| 容器组件 | 是否可获焦 | focusable默认值 | 走焦规则 | 251| ---------------------------------------- | ----- | ------------ | -------- | 252| [AbilityComponent](../reference/arkui-ts/ts-container-ability-component.md) | 否 | false | / | 253| [Badge](../reference/arkui-ts/ts-container-badge.md) | 否 | false | / | 254| [Column](../reference/arkui-ts/ts-container-column.md) | 是 | true | 线性走焦 | 255| [ColumnSplit](../reference/arkui-ts/ts-container-columnsplit.md) | 是 | true | / | 256| [Counter](../reference/arkui-ts/ts-container-counter.md) | 是 | true | 线性走焦 | 257| [Flex](../reference/arkui-ts/ts-container-flex.md) | 是 | true | 线性走焦 | 258| [GridCol](../reference/arkui-ts/ts-container-gridcol.md) | 是 | true | 容器组件自定义 | 259| [GridRow](../reference/arkui-ts/ts-container-gridrow.md) | 是 | true | 容器组件自定义 | 260| [Grid](../reference/arkui-ts/ts-container-grid.md) | 是 | true | 容器组件自定义 | 261| [GridItem](../reference/arkui-ts/ts-container-griditem.md) | 是 | true | 跟随子组件 | 262| [List](../reference/arkui-ts/ts-container-list.md) | 是 | true | 线性走焦 | 263| [ListItem](../reference/arkui-ts/ts-container-listitem.md) | 是 | true | 跟随子组件 | 264| [ListItemGroup](../reference/arkui-ts/ts-container-listitemgroup.md) | 是 | true | 跟随List组件 | 265| [Navigator](../reference/arkui-ts/ts-container-navigator.md) | 否 | true | 容器组件自定义 | 266| [Panel](../reference/arkui-ts/ts-container-panel.md) | 否 | true | 跟随子组件 | 267| [Refresh](../reference/arkui-ts/ts-container-refresh.md) | 否 | false | / | 268| [RelativeContainer](../reference/arkui-ts/ts-container-relativecontainer.md) | 否 | true | 容器组件自定义 | 269| [Row](../reference/arkui-ts/ts-container-row.md) | 是 | true | 线性走焦 | 270| [RowSplit](../reference/arkui-ts/ts-container-rowsplit.md) | 是 | true | / | 271| [Scroll](../reference/arkui-ts/ts-container-scroll.md) | 是 | true | 线性走焦 | 272| [SideBarContainer](../reference/arkui-ts/ts-container-sidebarcontainer.md) | 是 | true | 线性走焦 | 273| [Stack](../reference/arkui-ts/ts-container-stack.md) | 是 | true | 线性走焦 | 274| [Swiper](../reference/arkui-ts/ts-container-swiper.md) | 是 | true | 容器组件自定义 | 275| [Tabs](../reference/arkui-ts/ts-container-tabs.md) | 是 | true | 容器组件自定义 | 276| [TabContent](../reference/arkui-ts/ts-container-tabcontent.md) | 是 | true | 跟随子组件 | 277 278 **表3** 媒体组件获焦能力 279 280| 媒体组件 | 是否可获焦 | focusable默认值 | 走焦规则 | 281| ---------------------------------------- | ----- | ------------ | ---- | 282| [Video](../reference/arkui-ts/ts-media-components-video.md) | 是 | true | / | 283 284 **表4** 画布组件获焦能力 285 286| 画布组件 | 是否可获焦 | focusable默认值 | 走焦规则 | 287| ---------------------------------------- | ----- | ------------ | ---- | 288| [Canvas](../reference/arkui-ts/ts-components-canvas-canvas.md) | 否 | false | / | 289 290 291以下示例为大家展示focusable接口的使用方法: 292 293 294 295```ts 296// xxx.ets 297@Entry 298@Component 299struct FocusableExample { 300 @State textFocusable: boolean = true; 301 @State color1: Color = Color.Yellow; 302 @State color2: Color = Color.Yellow; 303 304 build() { 305 Column({ space: 5 }) { 306 Text('Default Text') // 第一个Text组件未设置focusable属性,默认不可获焦 307 .borderColor(this.color1) 308 .borderWidth(2) 309 .width(300) 310 .height(70) 311 .onFocus(() => { 312 this.color1 = Color.Blue; 313 }) 314 .onBlur(() => { 315 this.color1 = Color.Yellow; 316 }) 317 Divider() 318 319 Text('focusable: ' + this.textFocusable) // 第二个Text设置了focusable属性,初始值为true 320 .borderColor(this.color2) 321 .borderWidth(2) 322 .width(300) 323 .height(70) 324 .focusable(this.textFocusable) 325 .onFocus(() => { 326 this.color2 = Color.Blue; 327 }) 328 .onBlur(() => { 329 this.color2 = Color.Yellow; 330 }) 331 332 Divider() 333 334 Row() { 335 Button('Button1') 336 .width(140).height(70) 337 Button('Button2') 338 .width(160).height(70) 339 } 340 341 Divider() 342 Button('Button3') 343 .width(300).height(70) 344 345 Divider() 346 }.width('100%').justifyContent(FlexAlign.Center) 347 .onKeyEvent((e) => { // 绑定onKeyEvent,在该Column组件获焦时,按下'F'键,可将第二个Text的focusable置反 348 if (e.keyCode === 2022 && e.type === KeyType.Down) { 349 this.textFocusable = !this.textFocusable; 350 } 351 }) 352 } 353} 354``` 355 356 357运行效果: 358 359 360![zh-cn_image_0000001511900540](figures/zh-cn_image_0000001511900540.gif) 361 362 363上述示例包含默认获焦和主动走焦两部分: 364 365 366**默认获焦:** 367 368 369- 根据默认焦点的说明,该应用打开后,默认第一个可获焦元素获焦: 370 371- 第一个Text组件没有设置focusable(true)属性,该Text组件无法获焦。 372 373- 第二个Text组件的focusable属性显式设置为true,说明该组件可获焦,那么默认焦点将置到它身上。 374 375 376**主动走焦:** 377 378 379按键盘F键,触发onKeyEvent,focusable置为false,Text组件变成不可获焦,焦点自动转移,按照被动走焦中的说明项,焦点会自动从Text组件先向上寻找下一个可获焦组件,由于上一个组件是一个不可获焦的Text,所以向下寻找下一个可获焦的组件,找到并使焦点转移到Row容器上,根据[走焦至容器规则](#走焦规则),计算Button1和Button2的位置,Button2比Button1更大,因此焦点会自动转移到Button2上。 380 381 382## 自定义默认焦点 383 384 385```ts 386defaultFocus(value: boolean) 387``` 388 389焦点系统在页面初次构建完成时,会搜索当前页下的所有组件,找到第一个绑定了defaultFocus(true)的组件,然后将该组件置为默认焦点,若无任何组件绑定defaultFocus(true),则将第一个找到的可获焦的组件置为默认焦点。 390 391以如下应用为例,应用布局如下: 392 393![zh-cn_image_0000001563060793](figures/zh-cn_image_0000001563060793.png) 394 395以下是实现该应用的示例代码,且示例代码中没有设置defaultFocus: 396 397 398```ts 399// xxx.ets 400import promptAction from '@ohos.promptAction'; 401 402class MyDataSource implements IDataSource { 403 private list: number[] = []; 404 private listener: DataChangeListener; 405 406 constructor(list: number[]) { 407 this.list = list; 408 } 409 410 totalCount(): number { 411 return this.list.length; 412 } 413 414 getData(index: number): any { 415 return this.list[index]; 416 } 417 418 registerDataChangeListener(listener: DataChangeListener): void { 419 this.listener = listener; 420 } 421 422 unregisterDataChangeListener() { 423 } 424} 425 426@Entry 427@Component 428struct SwiperExample { 429 private swiperController: SwiperController = new SwiperController() 430 private data: MyDataSource = new MyDataSource([]) 431 432 aboutToAppear(): void { 433 let list = [] 434 for (let i = 1; i <= 4; i++) { 435 list.push(i.toString()); 436 } 437 this.data = new MyDataSource(list); 438 } 439 440 build() { 441 Column({ space: 5 }) { 442 Swiper(this.swiperController) { 443 LazyForEach(this.data, (item: string) => { 444 Row({ space: 20 }) { 445 Column() { 446 Button('1').width(200).height(200) 447 .fontSize(40) 448 .backgroundColor('#dadbd9') 449 } 450 451 Column({ space: 20 }) { 452 Row({ space: 20 }) { 453 Button('2') 454 .width(100) 455 .height(100) 456 .fontSize(40) 457 .type(ButtonType.Normal) 458 .borderRadius(20) 459 .backgroundColor('#dadbd9') 460 Button('3') 461 .width(100) 462 .height(100) 463 .fontSize(40) 464 .type(ButtonType.Normal) 465 .borderRadius(20) 466 .backgroundColor('#dadbd9') 467 } 468 469 Row({ space: 20 }) { 470 Button('4') 471 .width(100) 472 .height(100) 473 .fontSize(40) 474 .type(ButtonType.Normal) 475 .borderRadius(20) 476 .backgroundColor('#dadbd9') 477 Button('5') 478 .width(100) 479 .height(100) 480 .fontSize(40) 481 .type(ButtonType.Normal) 482 .borderRadius(20) 483 .backgroundColor('#dadbd9') 484 } 485 486 Row({ space: 20 }) { 487 Button('6') 488 .width(100) 489 .height(100) 490 .fontSize(40) 491 .type(ButtonType.Normal) 492 .borderRadius(20) 493 .backgroundColor('#dadbd9') 494 Button('7') 495 .width(100) 496 .height(100) 497 .fontSize(40) 498 .type(ButtonType.Normal) 499 .borderRadius(20) 500 .backgroundColor('#dadbd9') 501 } 502 } 503 } 504 .width(480) 505 .height(380) 506 .justifyContent(FlexAlign.Center) 507 .borderWidth(2) 508 .borderColor(Color.Gray) 509 .backgroundColor(Color.White) 510 }, item => item) 511 } 512 .cachedCount(2) 513 .index(0) 514 .interval(4000) 515 .indicator(true) 516 .loop(true) 517 .duration(1000) 518 .itemSpace(0) 519 .curve(Curve.Linear) 520 .onChange((index: number) => { 521 console.info(index.toString()); 522 }) 523 .margin({ left: 20, top: 20, right: 20 }) 524 525 Row({ space: 40 }) { 526 Button('←') 527 .fontSize(40) 528 .fontWeight(FontWeight.Bold) 529 .fontColor(Color.Black) 530 .backgroundColor(Color.Transparent) 531 .onClick(() => { 532 this.swiperController.showPrevious(); 533 }) 534 Button('→') 535 .fontSize(40) 536 .fontWeight(FontWeight.Bold) 537 .fontColor(Color.Black) 538 .backgroundColor(Color.Transparent) 539 .onClick(() => { 540 this.swiperController.showNext(); 541 }) 542 } 543 .width(480) 544 .height(50) 545 .justifyContent(FlexAlign.Center) 546 .borderWidth(2) 547 .borderColor(Color.Gray) 548 .backgroundColor('#f7f6dc') 549 550 Row({ space: 40 }) { 551 Button('Cancel') 552 .fontSize(30) 553 .fontColor('#787878') 554 .type(ButtonType.Normal) 555 .width(140) 556 .height(50) 557 .backgroundColor('#dadbd9') 558 559 Button('OK') 560 .fontSize(30) 561 .fontColor('#787878') 562 .type(ButtonType.Normal) 563 .width(140) 564 .height(50) 565 .backgroundColor('#dadbd9') 566 .onClick(() => { 567 promptAction.showToast({ message: 'Button OK on clicked' }); 568 }) 569 } 570 .width(480) 571 .height(80) 572 .justifyContent(FlexAlign.Center) 573 .borderWidth(2) 574 .borderColor(Color.Gray) 575 .backgroundColor('#dff2e4') 576 .margin({ left: 20, bottom: 20, right: 20 }) 577 }.backgroundColor('#f2f2f2') 578 .margin({ left: 50, top: 50, right: 20 }) 579 } 580} 581``` 582 583 584当前应用上无任何defaultFocus设置,所以第一个可获焦的组件默认获取焦点,按下TAB键/方向键让获焦的组件显示焦点态样式: 585 586 587![zh-cn_image_0000001511421360](figures/zh-cn_image_0000001511421360.gif) 588 589 590假设开发者想让应用打开的时候,无需执行多余的切换焦点操作,直接点击按键的空格/回车键,就可以执行Button-OK的onClick回调操作,那么就可以给这个Button绑定defaultFocus(true),让它成为该页面上的默认焦点: 591 592 593 594```ts 595Button('OK') 596 .defaultFocus(true) // 设置Button-OK为defaultFocus 597 .fontSize(30) 598 .fontColor('#787878') 599 .type(ButtonType.Normal) 600 .width(140).height(50).backgroundColor('#dadbd9') 601 .onClick(() => { 602 promptAction.showToast({ message: 'Button OK on clicked' }); 603 }) 604``` 605 606 607![zh-cn_image_0000001562940617](figures/zh-cn_image_0000001562940617.gif) 608 609 610打开应用后按TAB键,Button-OK显示了焦点态,说明默认焦点变更到了Button-OK上。然后按下空格,响应了Button-OK的onClick事件。 611 612 613## 自定义TAB键走焦顺序 614 615 616```ts 617tabIndex(index: number) 618``` 619 620tabIndex用于设置自定义TAB键走焦顺序,默认值为0。使用“TAB/Shift+TAB键”走焦时(方向键不影响),系统会自动获取到所有配置了tabIndex大于0的组件,然后按照递增/递减排序进行走焦。 621 622 623以[defaultFocus](#自定义默认焦点)提供的示例为例,默认情况下的走焦顺序如下: 624 625 626![zh-cn_image_0000001511421364](figures/zh-cn_image_0000001511421364.gif) 627 628 629默认的走焦顺序从第一个获焦组件一路走到最后一个获焦组件,会经历Button1->Button4->Button5->Button7->左箭头->右箭头->ButtonOK。这种走焦队列比较完整,遍历了大部分的组件。但缺点是从第一个走到最后一个所经历的路径较长。 630 631 632如果想实现快速的从第一个走到最后一个,又不想牺牲太多的遍历完整性,就可以使用tabIndex通用属性。 633 634 635比如:开发者把白色的区域当为一个整体,黄色的区域当为一个整体,绿色的区域当为一个整体,实现Button1->左箭头->ButtonOK这种队列的走焦顺序,只需要在Button1、左箭头、ButtonOK这三个组件上依次增加tabIndex(1)、tabIndex(2)、tabIndex(3)。tabIndex的参数表示TAB走焦的顺序(从大于0的数字开始,从小到大排列)。 636 637 638 639```ts 640 Button('1').width(200).height(200) 641 .fontSize(40) 642 .backgroundColor('#dadbd9') 643 .tabIndex(1) // Button-1设置为第一个tabIndex节点 644``` 645 646 647 648```ts 649 Button('←') 650 .fontSize(40) 651 .fontWeight(FontWeight.Bold) 652 .fontColor(Color.Black) 653 .backgroundColor(Color.Transparent) 654 .onClick(() => { 655 this.swiperController.showPrevious(); 656 }) 657 .tabIndex(2) // Button-左箭头设置为第二个tabIndex节点 658``` 659 660 661 662```ts 663Button('OK') 664 .fontSize(30) 665 .fontColor('#787878') 666 .type(ButtonType.Normal) 667 .width(140).height(50).backgroundColor('#dadbd9') 668 .onClick(() => { 669 promptAction.showToast({ message: 'Button OK on clicked' }); 670 }) 671 .tabIndex(3) // Button-OK设置为第三个tabIndex节点 672``` 673 674 675![zh-cn_image_0000001511580976](figures/zh-cn_image_0000001511580976.gif) 676 677 678>**说明:** 679> - 当焦点处于tabIndex(大于0)节点上时,TAB/ShiftTAB会优先在tabIndex(大于0)的队列中寻找后置/前置的节点,存在则走焦至相应的tabIndex节点。若不存在,则使用默认的走焦逻辑继续往后/往前走焦。 680> 681> - 当焦点处于tabIndex(等于0)节点上时,TAB/ShiftTAB使用默认的走焦逻辑走焦,走焦的过程中会跳过tabIndex(大于0)和tabIndex(小于0)的节点。 682> 683> - 当焦点处于tabIndex(小于0)节点上时,TAB/ShiftTAB无法走焦。 684 685 686### groupDefaultFocus 687 688 689```ts 690groupDefaultFocus(value: boolean) 691``` 692 693[自定义TAB键走焦顺序](#自定义tab键走焦顺序)中所展示的使用tabIndex完成快速走焦的能力有如下问题: 694 695每个区域(白色/黄色/绿色三个区域)都设置了某个组件为tabIndex节点(白色-Button1、黄色-左箭头、绿色-ButtonOK),但这样设置之后,只能在这3个组件上按TAB/ShiftTab键走焦时会有快速走焦的效果。 696 697解决方案是给每个区域的容器设置tabIndex,但是这样设置的问题是:第一次走焦到容器上时,获焦的子组件是默认的第一个可获焦组件,并不是自己想要的组件(Button1、左箭头、ButtonOK)。 698 699这样便引入了groupDefaultFocus通用属性,参数:boolean,默认值:false。 700 701用法需和tabIndex组合使用,使用tabIndex给区域(容器)绑定走焦顺序,然后给Button1、左箭头、ButtonOK绑定groupDefaultFocus(true),这样在首次走焦到目标区域(容器)上时,它的绑定了groupDefaultFocus(true)的子组件同时获得焦点。 702 703 704```ts 705// xxx.ets 706import promptAction from '@ohos.promptAction'; 707 708class MyDataSource implements IDataSource { 709 private list: number[] = []; 710 private listener: DataChangeListener; 711 712 constructor(list: number[]) { 713 this.list = list; 714 } 715 716 totalCount(): number { 717 return this.list.length; 718 } 719 720 getData(index: number): any { 721 return this.list[index]; 722 } 723 724 registerDataChangeListener(listener: DataChangeListener): void { 725 this.listener = listener; 726 } 727 728 unregisterDataChangeListener() { 729 } 730} 731 732@Entry 733@Component 734struct SwiperExample { 735 private swiperController: SwiperController = new SwiperController() 736 private data: MyDataSource = new MyDataSource([]) 737 738 aboutToAppear(): void { 739 let list = [] 740 for (let i = 1; i <= 4; i++) { 741 list.push(i.toString()); 742 } 743 this.data = new MyDataSource(list); 744 } 745 746 build() { 747 Column({ space: 5 }) { 748 Swiper(this.swiperController) { 749 LazyForEach(this.data, (item: string) => { 750 Row({ space: 20 }) { // 设置该Row组件为tabIndex的第一个节点 751 Column() { 752 Button('1').width(200).height(200) 753 .fontSize(40) 754 .backgroundColor('#dadbd9') 755 .groupDefaultFocus(true) // 设置Button-1为第一个tabIndex的默认焦点 756 } 757 758 Column({ space: 20 }) { 759 Row({ space: 20 }) { 760 Button('2') 761 .width(100) 762 .height(100) 763 .fontSize(40) 764 .type(ButtonType.Normal) 765 .borderRadius(20) 766 .backgroundColor('#dadbd9') 767 Button('3') 768 .width(100) 769 .height(100) 770 .fontSize(40) 771 .type(ButtonType.Normal) 772 .borderRadius(20) 773 .backgroundColor('#dadbd9') 774 } 775 776 Row({ space: 20 }) { 777 Button('4') 778 .width(100) 779 .height(100) 780 .fontSize(40) 781 .type(ButtonType.Normal) 782 .borderRadius(20) 783 .backgroundColor('#dadbd9') 784 Button('5') 785 .width(100) 786 .height(100) 787 .fontSize(40) 788 .type(ButtonType.Normal) 789 .borderRadius(20) 790 .backgroundColor('#dadbd9') 791 } 792 793 Row({ space: 20 }) { 794 Button('6') 795 .width(100) 796 .height(100) 797 .fontSize(40) 798 .type(ButtonType.Normal) 799 .borderRadius(20) 800 .backgroundColor('#dadbd9') 801 Button('7') 802 .width(100) 803 .height(100) 804 .fontSize(40) 805 .type(ButtonType.Normal) 806 .borderRadius(20) 807 .backgroundColor('#dadbd9') 808 } 809 } 810 } 811 .width(480) 812 .height(380) 813 .justifyContent(FlexAlign.Center) 814 .borderWidth(2) 815 .borderColor(Color.Gray) 816 .backgroundColor(Color.White) 817 .tabIndex(1) 818 }, item => item) 819 } 820 .cachedCount(2) 821 .index(0) 822 .interval(4000) 823 .indicator(true) 824 .loop(true) 825 .duration(1000) 826 .itemSpace(0) 827 .curve(Curve.Linear) 828 .onChange((index: number) => { 829 console.info(index.toString()); 830 }) 831 .margin({ left: 20, top: 20, right: 20 }) 832 833 Row({ space: 40 }) { // 设置该Row组件为第二个tabIndex节点 834 Button('←') 835 .fontSize(40) 836 .fontWeight(FontWeight.Bold) 837 .fontColor(Color.Black) 838 .backgroundColor(Color.Transparent) 839 .onClick(() => { 840 this.swiperController.showPrevious(); 841 }) 842 .groupDefaultFocus(true) // 设置Button-左箭头为第二个tabIndex节点的默认焦点 843 Button('→') 844 .fontSize(40) 845 .fontWeight(FontWeight.Bold) 846 .fontColor(Color.Black) 847 .backgroundColor(Color.Transparent) 848 .onClick(() => { 849 this.swiperController.showNext(); 850 }) 851 } 852 .width(480) 853 .height(50) 854 .justifyContent(FlexAlign.Center) 855 .borderWidth(2) 856 .borderColor(Color.Gray) 857 .backgroundColor('#f7f6dc') 858 .tabIndex(2) 859 860 Row({ space: 40 }) { // 设置该Row组件为第三个tabIndex节点 861 Button('Cancel') 862 .fontSize(30) 863 .fontColor('#787878') 864 .type(ButtonType.Normal) 865 .width(140) 866 .height(50) 867 .backgroundColor('#dadbd9') 868 869 Button('OK') 870 .fontSize(30) 871 .fontColor('#787878') 872 .type(ButtonType.Normal) 873 .width(140) 874 .height(50) 875 .backgroundColor('#dadbd9') 876 .defaultFocus(true) 877 .onClick(() => { 878 promptAction.showToast({ message: 'Button OK on clicked' }); 879 }) 880 .groupDefaultFocus(true) // 设置Button-OK为第三个tabIndex节点的默认焦点 881 } 882 .width(480) 883 .height(80) 884 .justifyContent(FlexAlign.Center) 885 .borderWidth(2) 886 .borderColor(Color.Gray) 887 .backgroundColor('#dff2e4') 888 .margin({ left: 20, bottom: 20, right: 20 }) 889 .tabIndex(3) 890 }.backgroundColor('#f2f2f2') 891 .margin({ left: 50, top: 50, right: 20 }) 892 } 893} 894``` 895 896![zh-cn_image_0000001562700533](figures/zh-cn_image_0000001562700533.gif) 897 898 899### focusOnTouch 900 901 902```ts 903focusOnTouch(value: boolean) 904``` 905 906点击获焦能力,参数:boolean,默认值:false(输入类组件:TextInput、TextArea、Search、Web默认值是true)。 907 908点击是指使用触屏或鼠标左键进行单击,默认为false的组件,例如Button,不绑定该API时,点击Button不会使其获焦,当给Button绑定focusOnTouch(true)时,点击Button会使Button立即获得焦点。 909 910给容器绑定focusOnTouch(true)时,点击容器区域,会立即使容器的第一个可获焦组件获得焦点。 911 912示例代码: 913 914 915```ts 916// requestFocus.ets 917import promptAction from '@ohos.promptAction'; 918 919@Entry 920@Component 921struct RequestFocusExample { 922 @State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N'] 923 924 build() { 925 Column({ space:20 }){ 926 Button("id: " + this.idList[0] + " focusOnTouch(true) + focusable(false)") 927 .width(400).height(70).fontColor(Color.White).focusOnTouch(true) 928 .focusable(false) 929 Button("id: " + this.idList[1] + " default") 930 .width(400).height(70).fontColor(Color.White) 931 Button("id: " + this.idList[2] + " focusOnTouch(false)") 932 .width(400).height(70).fontColor(Color.White).focusOnTouch(false) 933 Button("id: " + this.idList[3] + " focusOnTouch(true)") 934 .width(400).height(70).fontColor(Color.White).focusOnTouch(true) 935 }.width('100%').margin({ top:20 }) 936 } 937} 938``` 939 940 941![zh-cn_image_0000001511580980](figures/zh-cn_image_0000001511580980.gif) 942 943 944解读: 945 946 947Button-A虽然设置了focusOnTouch(true),但是同时也设置了focusable(false),该组件无法获焦,因此点击后也无法获焦; 948 949 950Button-B不设置相关属性,点击后不会获焦; 951 952 953Button-C设置了focusOnTouch(false),同Button-B,点击后也不会获焦; 954 955 956Button-D设置了focusOnTouch(true),点击即可使其获焦; 957 958 959>**说明:** 960> 961>由于焦点态的阐述的特性,焦点态在屏幕接收点击事件后会立即清除。因此该示例代码在每次点击后,需要再次按下TAB键使焦点态再次显示,才可知道当前焦点所在的组件。 962 963 964### focusControl.requestFocus 965 966 967```ts 968focusControl.requestFocus(id: string) 969``` 970 971主动申请焦点能力的全局方法,参数:string,参数表示被申请组件的id(通用属性id设置的字符串)。 972 973 974使用方法为:在任意执行语句中调用该API,指定目标组件的id为方法参数,当程序执行到该语句时,会立即给指定的目标组件申请焦点。 975 976 977代码示例: 978 979 980 981```ts 982// requestFocus.ets 983import promptAction from '@ohos.promptAction'; 984 985@Entry 986@Component 987struct RequestFocusExample { 988 @State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N'] 989 @State requestId: number = 0 990 991 build() { 992 Column({ space:20 }){ 993 Row({space: 5}) { 994 Button("id: " + this.idList[0] + " focusable(false)") 995 .width(200).height(70).fontColor(Color.White) 996 .id(this.idList[0]) 997 .focusable(false) 998 Button("id: " + this.idList[1]) 999 .width(200).height(70).fontColor(Color.White) 1000 .id(this.idList[1]) 1001 } 1002 Row({space: 5}) { 1003 Button("id: " + this.idList[2]) 1004 .width(200).height(70).fontColor(Color.White) 1005 .id(this.idList[2]) 1006 Button("id: " + this.idList[3]) 1007 .width(200).height(70).fontColor(Color.White) 1008 .id(this.idList[3]) 1009 } 1010 Row({space: 5}) { 1011 Button("id: " + this.idList[4]) 1012 .width(200).height(70).fontColor(Color.White) 1013 .id(this.idList[4]) 1014 Button("id: " + this.idList[5]) 1015 .width(200).height(70).fontColor(Color.White) 1016 .id(this.idList[5]) 1017 } 1018 }.width('100%').margin({ top:20 }) 1019 .onKeyEvent((e) => { 1020 if (e.keyCode >= 2017 && e.keyCode <= 2022) { 1021 this.requestId = e.keyCode - 2017; 1022 } else if (e.keyCode === 2030) { 1023 this.requestId = 6; 1024 } else { 1025 return; 1026 } 1027 if (e.type !== KeyType.Down) { 1028 return; 1029 } 1030 let res = focusControl.requestFocus(this.idList[this.requestId]); 1031 if (res) { 1032 promptAction.showToast({message: 'Request success'}); 1033 } else { 1034 promptAction.showToast({message: 'Request failed'}); 1035 } 1036 }) 1037 } 1038} 1039``` 1040 1041 1042![zh-cn_image_0000001562820905](figures/zh-cn_image_0000001562820905.gif) 1043 1044 1045解读:页面中共6个Button组件,其中Button-A组件设置了focusable(false),表示其不可获焦,在外部容器的onKeyEvent中,监听按键事件,当按下A ~ F按键时,分别去申请Button A ~ F 的焦点,另外按下N键,是给当前页面上不存在的id的组件去申请焦点。 1046 1047 10481. 按下TAB键,由于第一个组件Button-A设置了无法获焦,那么默认第二个组件Button-B获焦,Button-B展示焦点态样式; 1049 10502. 键盘上按下A键,申请Button-A的焦点,气泡显示Request failed,表示无法获取到焦点,焦点位置未改变; 1051 10523. 键盘上按下B键,申请Button-B的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置原本就在Button-B,位置未改变; 1053 10544. 键盘上按下C键,申请Button-C的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-B变更为Button-C; 1055 10565. 键盘上按下D键,申请Button-D的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-C变更为Button-D; 1057 10586. 键盘上按下E键,申请Button-E的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-D变更为Button-E; 1059 10607. 键盘上按下F键,申请Button-F的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-E变更为Button-F; 1061 10628. 键盘上按下N键,申请未知组件的焦点,气泡显示Request failed,表示无法获取到焦点,焦点位置不变; 1063