鴻蒙HarmonyOS實戰-ArkUI事件(焦點事件)

蜀道山QAQ發表於2024-05-07

🚀前言

焦點事件是指程式中的重要事件或關鍵點。焦點事件通常是程式的核心邏輯和功能,需要引起特殊的關注和處理。

在圖形使用者介面(GUI)程式設計中,焦點事件通常與使用者互動和介面輸入相關。例如,當使用者點選按鈕、輸入文字或選擇選單項時,這些操作會觸發相應的焦點事件。程式需要捕獲這些焦點事件並進行處理,以執行相應的操作或響應使用者的請求。

另外,在事件驅動的程式設計模型中,焦點事件也與程式的流程控制和狀態轉換有關。例如,當某個關鍵條件滿足時,程式會觸發相應的焦點事件,然後根據這些事件來執行特定的操作或改變程式的狀態。

🚀一、焦點事件

🔎1.基本概念

焦點事件基本概念是指在使用者介面中,焦點在不同控制元件之間切換時,觸發的相關事件。下面是一些焦點事件的基本概念:

  1. 焦點(Focus):焦點是指使用者當前正在與之互動的控制元件或元素。例如,在一個表單中,焦點可能位於輸入框、核取方塊或按鈕等控制元件上。焦點通常用來表示哪個控制元件可以接收使用者的輸入。

  2. 預設焦點(Default Focus):預設焦點是指使用者在進入一個介面或開啟一個應用程式時,自動設定在介面中某個控制元件上的焦點。預設焦點通常是用來提高使用者互動的效率,使使用者可以直接開始輸入或選擇操作。

  3. 獲焦(Focus Gained):獲焦是指當一個控制元件或元素成為焦點時觸發的事件。獲焦事件通常可以用來執行一些初始化操作,例如設定焦點控制元件的樣式或載入資料。

  4. 失焦(Focus Lost):失焦是指當一個控制元件或元素不再是焦點時觸發的事件。失焦事件通常可以用來執行一些清理操作,例如儲存使用者輸入或驗證輸入資料。

  5. 走焦(Traversal):走焦是指焦點在控制元件之間切換的過程。焦點可以透過按下Tab鍵或者使用方向鍵來在不同的控制元件之間移動。

  6. 焦點態(Focus State):焦點態是指控制元件或元素在成為焦點或失去焦點時,其外觀或狀態發生的變化。焦點態可以用來提高使用者互動的可見性,例如高亮顯示焦點控制元件或顯示輸入游標。

焦點事件基本概念涉及到焦點的獲取、失去和切換,以及與焦點相關的事件和狀態。

🔎2.走焦規則

image

🔎3.監聽元件的焦點變化

介面定義:

onFocus(event: () => void)//獲焦事件回撥
onBlur(event:() => void)//失焦事件回撥

案例:

// xxx.ets
@Entry
@Component
struct FocusEventExample {
  @State oneButtonColor: Color = Color.Gray;
  @State twoButtonColor: Color = Color.Gray;
  @State threeButtonColor: Color = Color.Gray;

  build() {
    Column({ space: 20 }) {
      // 透過外接鍵盤的上下鍵可以讓焦點在三個按鈕間移動,按鈕獲焦時顏色變化,失焦時變回原背景色
      Button('First Button')
        .width(260)
        .height(70)
        .backgroundColor(this.oneButtonColor)
        .fontColor(Color.Black)
          // 監聽第一個元件的獲焦事件,獲焦後改變顏色
        .onFocus(() => {
          this.oneButtonColor = Color.Green;
        })
          // 監聽第一個元件的失焦事件,失焦後改變顏色
        .onBlur(() => {
          this.oneButtonColor = Color.Gray;
        })

      Button('Second Button')
        .width(260)
        .height(70)
        .backgroundColor(this.twoButtonColor)
        .fontColor(Color.Black)
          // 監聽第二個元件的獲焦事件,獲焦後改變顏色
        .onFocus(() => {
          this.twoButtonColor = Color.Green;
        })
          // 監聽第二個元件的失焦事件,失焦後改變顏色
        .onBlur(() => {
          this.twoButtonColor = Color.Grey;
        })

      Button('Third Button')
        .width(260)
        .height(70)
        .backgroundColor(this.threeButtonColor)
        .fontColor(Color.Black)
          // 監聽第三個元件的獲焦事件,獲焦後改變顏色
        .onFocus(() => {
          this.threeButtonColor = Color.Green;
        })
          // 監聽第三個元件的失焦事件,失焦後改變顏色
        .onBlur(() => {
          this.threeButtonColor = Color.Gray ;
        })
    }.width('100%').margin({ top: 20 })
  }
}

image

🔎4.設定元件是否獲焦

image

按照元件的獲焦能力分為三類的表格展示,可以根據需要選擇適合的元件型別來實現焦點控制功能。

介面:

focusable(value: boolean)

案例:

// xxx.ets
@Entry
@Component
struct FocusableExample {
  @State textFocusable: boolean = true;
  @State color1: Color = Color.Yellow;
  @State color2: Color = Color.Yellow;

  build() {
    Column({ space: 5 }) {
      Text('Default Text')    // 第一個Text元件未設定focusable屬性,預設不可獲焦
        .borderColor(this.color1)
        .borderWidth(2)
        .width(300)
        .height(70)
        .onFocus(() => {
          this.color1 = Color.Blue;
        })
        .onBlur(() => {
          this.color1 = Color.Yellow;
        })
      Divider()

      Text('focusable: ' + this.textFocusable)    // 第二個Text設定了focusable屬性,初始值為true
        .borderColor(this.color2)
        .borderWidth(2)
        .width(300)
        .height(70)
        .focusable(this.textFocusable)
        .onFocus(() => {
          this.color2 = Color.Blue;
        })
        .onBlur(() => {
          this.color2 = Color.Yellow;
        })

      Divider()

      Row() {
        Button('Button1')
          .width(140).height(70)
        Button('Button2')
          .width(160).height(70)
      }

      Divider()
      Button('Button3')
        .width(300).height(70)

      Divider()
    }.width('100%').justifyContent(FlexAlign.Center)
    .onKeyEvent((e) => {    // 繫結onKeyEvent,在該Column元件獲焦時,按下'F'鍵,可將第二個Text的focusable置反
      if (e.keyCode === 2022 && e.type === KeyType.Down) {
        this.textFocusable = !this.textFocusable;
      }
    })
  }
}

image

🔎5.自定義預設焦點

介面:

defaultFocus(value: boolean)

案例:

// xxx.ets
import promptAction from '@ohos.promptAction';

class MyDataSource implements IDataSource {
  private list: number[] = [];
  private listener: DataChangeListener;

  constructor(list: number[]) {
    this.list = list;
  }

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): any {
    return this.list[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  unregisterDataChangeListener() {
  }
}

@Entry
@Component
struct SwiperExample {
  private swiperController: SwiperController = new SwiperController()
  private data: MyDataSource = new MyDataSource([])

  aboutToAppear(): void {
    let list = []
    for (let i = 1; i <= 4; i++) {
      list.push(i.toString());
    }
    this.data = new MyDataSource(list);
  }

  build() {
    Column({ space: 5 }) {
      Swiper(this.swiperController) {
        LazyForEach(this.data, (item: string) => {
          Row({ space: 20 }) {
            Column() {
              Button('1').width(200).height(200)
                .fontSize(40)
                .backgroundColor('#dadbd9')
            }

            Column({ space: 20 }) {
              Row({ space: 20 }) {
                Button('2')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
                Button('3')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
              }

              Row({ space: 20 }) {
                Button('4')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
                Button('5')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
              }

              Row({ space: 20 }) {
                Button('6')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
                Button('7')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
              }
            }
          }
          .width(480)
          .height(380)
          .justifyContent(FlexAlign.Center)
          .borderWidth(2)
          .borderColor(Color.Gray)
          .backgroundColor(Color.White)
        }, item => item)
      }
      .cachedCount(2)
      .index(0)
      .interval(4000)
      .indicator(true)
      .loop(true)
      .duration(1000)
      .itemSpace(0)
      .curve(Curve.Linear)
      .onChange((index: number) => {
        console.info(index.toString());
      })
      .margin({ left: 20, top: 20, right: 20 })

      Row({ space: 40 }) {
        Button('←')
          .fontSize(40)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.Black)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.swiperController.showPrevious();
          })
        Button('→')
          .fontSize(40)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.Black)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.swiperController.showNext();
          })
      }
      .width(480)
      .height(50)
      .justifyContent(FlexAlign.Center)
      .borderWidth(2)
      .borderColor(Color.Gray)
      .backgroundColor('#f7f6dc')

      Row({ space: 40 }) {
        Button('Cancel')
          .fontSize(30)
          .fontColor('#787878')
          .type(ButtonType.Normal)
          .width(140)
          .height(50)
          .backgroundColor('#dadbd9')

        Button('OK')
          .fontSize(30)
          .fontColor('#787878')
          .type(ButtonType.Normal)
          .width(140)
          .height(50)
          .backgroundColor('#dadbd9')
          .onClick(() => {
            promptAction.showToast({ message: 'Button OK on clicked' });
          })
      }
      .width(480)
      .height(80)
      .justifyContent(FlexAlign.Center)
      .borderWidth(2)
      .borderColor(Color.Gray)
      .backgroundColor('#dff2e4')
      .margin({ left: 20, bottom: 20, right: 20 })
    }.backgroundColor('#f2f2f2')
    .margin({ left: 50, top: 50, right: 20 })
  }
}

image

🔎6.自定義TAB鍵走焦順序

Button('1').width(200).height(200)
  .fontSize(40)
  .backgroundColor('#dadbd9')
  .tabIndex(1)    // Button-1設定為第一個tabIndex節點
Button('←')
  .fontSize(40)
  .fontWeight(FontWeight.Bold)
  .fontColor(Color.Black)
  .backgroundColor(Color.Transparent)
  .onClick(() => {
    this.swiperController.showPrevious();
  })
  .tabIndex(2)    // Button-左箭頭設定為第二個tabIndex節點
Button('OK')
  .fontSize(30)
  .fontColor('#787878')
  .type(ButtonType.Normal)
  .width(140).height(50).backgroundColor('#dadbd9')
  .onClick(() => {
    promptAction.showToast({ message: 'Button OK on clicked' });
  })
  .tabIndex(3)    // Button-OK設定為第三個tabIndex節點

🦋6.1 groupDefaultFocus

我們分別將某個元件設定為tabIndex節點,設定完之後,只有當我們按下TAB/ShiftTab鍵在這3個元件上進行焦點切換時,才會出現快速走焦的效果。

為了解決這個問題,我們可以給每個區域的容器設定tabIndex屬性。然而,這樣設定存在一個問題:當首次走焦到容器上時,焦點會預設落在容器內的第一個可獲焦元件上,而不是我們想要的Button1、左箭頭、ButtonOK。

為了解決這個問題,我們引入了一個名為groupDefaultFocus的通用屬性,該屬性接受一個布林值引數,預設值為false。使用該屬性需要與tabIndex屬性結合使用,首先使用tabIndex為每個區域(容器)定義焦點切換順序,然後為Button1、左箭頭、ButtonOK這些元件繫結groupDefaultFocus(true)。這樣,在首次走焦到目標區域(容器)時,擁有groupDefaultFocus(true)繫結的子元件將同時獲取焦點。

// xxx.ets
import promptAction from '@ohos.promptAction';

class MyDataSource implements IDataSource {
  private list: number[] = [];
  private listener: DataChangeListener;

  constructor(list: number[]) {
    this.list = list;
  }

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): any {
    return this.list[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  unregisterDataChangeListener() {
  }
}

@Entry
@Component
struct SwiperExample {
  private swiperController: SwiperController = new SwiperController()
  private data: MyDataSource = new MyDataSource([])

  aboutToAppear(): void {
    let list = []
    for (let i = 1; i <= 4; i++) {
      list.push(i.toString());
    }
    this.data = new MyDataSource(list);
  }

  build() {
    Column({ space: 5 }) {
      Swiper(this.swiperController) {
        LazyForEach(this.data, (item: string) => {
          Row({ space: 20 }) {    // 設定該Row元件為tabIndex的第一個節點
            Column() {
              Button('1').width(200).height(200)
                .fontSize(40)
                .backgroundColor('#dadbd9')
                .groupDefaultFocus(true)    // 設定Button-1為第一個tabIndex的預設焦點
            }

            Column({ space: 20 }) {
              Row({ space: 20 }) {
                Button('2')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
                Button('3')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
              }

              Row({ space: 20 }) {
                Button('4')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
                Button('5')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
              }

              Row({ space: 20 }) {
                Button('6')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
                Button('7')
                  .width(100)
                  .height(100)
                  .fontSize(40)
                  .type(ButtonType.Normal)
                  .borderRadius(20)
                  .backgroundColor('#dadbd9')
              }
            }
          }
          .width(480)
          .height(380)
          .justifyContent(FlexAlign.Center)
          .borderWidth(2)
          .borderColor(Color.Gray)
          .backgroundColor(Color.White)
          .tabIndex(1)
        }, item => item)
      }
      .cachedCount(2)
      .index(0)
      .interval(4000)
      .indicator(true)
      .loop(true)
      .duration(1000)
      .itemSpace(0)
      .curve(Curve.Linear)
      .onChange((index: number) => {
        console.info(index.toString());
      })
      .margin({ left: 20, top: 20, right: 20 })

      Row({ space: 40 }) {    // 設定該Row元件為第二個tabIndex節點
        Button('←')
          .fontSize(40)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.Black)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.swiperController.showPrevious();
          })
          .groupDefaultFocus(true)    // 設定Button-左箭頭為第二個tabIndex節點的預設焦點
        Button('→')
          .fontSize(40)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.Black)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.swiperController.showNext();
          })
      }
      .width(480)
      .height(50)
      .justifyContent(FlexAlign.Center)
      .borderWidth(2)
      .borderColor(Color.Gray)
      .backgroundColor('#f7f6dc')
      .tabIndex(2)

      Row({ space: 40 }) {    // 設定該Row元件為第三個tabIndex節點
        Button('Cancel')
          .fontSize(30)
          .fontColor('#787878')
          .type(ButtonType.Normal)
          .width(140)
          .height(50)
          .backgroundColor('#dadbd9')

        Button('OK')
          .fontSize(30)
          .fontColor('#787878')
          .type(ButtonType.Normal)
          .width(140)
          .height(50)
          .backgroundColor('#dadbd9')
          .defaultFocus(true)
          .onClick(() => {
            promptAction.showToast({ message: 'Button OK on clicked' });
          })
          .groupDefaultFocus(true)    // 設定Button-OK為第三個tabIndex節點的預設焦點
      }
      .width(480)
      .height(80)
      .justifyContent(FlexAlign.Center)
      .borderWidth(2)
      .borderColor(Color.Gray)
      .backgroundColor('#dff2e4')
      .margin({ left: 20, bottom: 20, right: 20 })
      .tabIndex(3)
    }.backgroundColor('#f2f2f2')
    .margin({ left: 50, top: 50, right: 20 })
  }
}

🦋6.2 focusOnTouch

介面:

focusOnTouch(value: boolean)

點選是指使用觸屏或滑鼠左鍵進行單擊,預設為false的元件,例如Button,不繫結該API時,點選Button不會使其獲焦,當給Button繫結focusOnTouch(true)時,點選Button會使Button立即獲得焦點。

案例:

// requestFocus.ets
import promptAction from '@ohos.promptAction';

@Entry
@Component
struct RequestFocusExample {
  @State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N']

  build() {
    Column({ space:20 }){
      Button("id: " + this.idList[0] + " focusOnTouch(true) + focusable(false)")
        .width(400).height(70).fontColor(Color.White).focusOnTouch(true)
        .focusable(false)
      Button("id: " + this.idList[1] + " default")
        .width(400).height(70).fontColor(Color.White)
      Button("id: " + this.idList[2] + " focusOnTouch(false)")
        .width(400).height(70).fontColor(Color.White).focusOnTouch(false)
      Button("id: " + this.idList[3] + " focusOnTouch(true)")
        .width(400).height(70).fontColor(Color.White).focusOnTouch(true)
    }.width('100%').margin({ top:20 })
  }
}

🦋6.3 focusControl.requestFocus

在任意執行語句中呼叫該API,指定目標元件的id為方法引數,當程式執行到該語句時,會立即給指定的目標元件申請焦點。

介面:

focusControl.requestFocus(id: string)

案例:

// requestFocus.ets
import promptAction from '@ohos.promptAction';

@Entry
@Component
struct RequestFocusExample {
  @State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N']
  @State requestId: number = 0

  build() {
    Column({ space:20 }){
      Row({space: 5}) {
        Button("id: " + this.idList[0] + " focusable(false)")
          .width(200).height(70).fontColor(Color.White)
          .id(this.idList[0])
          .focusable(false)
        Button("id: " + this.idList[1])
          .width(200).height(70).fontColor(Color.White)
          .id(this.idList[1])
      }
      Row({space: 5}) {
        Button("id: " + this.idList[2])
          .width(200).height(70).fontColor(Color.White)
          .id(this.idList[2])
        Button("id: " + this.idList[3])
          .width(200).height(70).fontColor(Color.White)
          .id(this.idList[3])
      }
      Row({space: 5}) {
        Button("id: " + this.idList[4])
          .width(200).height(70).fontColor(Color.White)
          .id(this.idList[4])
        Button("id: " + this.idList[5])
          .width(200).height(70).fontColor(Color.White)
          .id(this.idList[5])
      }
    }.width('100%').margin({ top:20 })
    .onKeyEvent((e) => {
      if (e.keyCode >= 2017 && e.keyCode <= 2022) {
        this.requestId = e.keyCode - 2017;
      } else if (e.keyCode === 2030) {
        this.requestId = 6;
      } else {
        return;
      }
      if (e.type !== KeyType.Down) {
        return;
      }
      let res = focusControl.requestFocus(this.idList[this.requestId]);
      if (res) {
        promptAction.showToast({message: 'Request success'});
      } else {
        promptAction.showToast({message: 'Request failed'});
      }
    })
  }
}

依次按下 TAB、A、B、C、D、E、F、N
image

🚀寫在最後

  • 如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
  • 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。
  • 關注小編,同時可以期待後續文章ing🚀,不定期分享原創知識。
  • 更多鴻蒙最新技術知識點,請關注作者部落格:https://t.doruo.cn/14DjR1rEY

image

相關文章