一、切换发送消息快捷键场景适配

场景介绍

IM(Instant Messaging,即时通讯)类应用在发送消息时,通常需要使用Enter键或Ctrl+Enter键作为发送消息快捷键,在使用TextArea/RichEditor组件作为输入组件时,Enter按键事件会被输入组件消费导致无法正确在onKeyEvent事件中响应事件。针对此场景,可以在应用内维护Ctrl键按下状态,通过keyEvent事件刷新状态,并使用输入组件的onSubmit回调作为响应入口,正确响应消息发送事件。

功能介绍

1、维护Ctrl按键状态并通过keyEvent事件刷新:

@State ctrlFlag: boolean = false; // 维护ctrl按键状态

.onKeyEvent((event: KeyEvent) => {
  if (event.getModifierKeyState) {
    // 每次keyEvent更新ctrl按键状态
    this.ctrlFlag = event.getModifierKeyState(['Ctrl']);
  }
})

2、响应onSubmit事件发送消息:

.enterKeyType(this.enterTypes[this.switchIndex]) // 通过switchIndex切换当前组件的enterKeyType
.onSubmit((enterKey: EnterKeyType, event: SubmitEvent) => {
  console.info('onSubmit', enterKey);
  // 通过enterKeyType和ctrl状态判断当前执行发送还是换行逻辑
  if ((enterKey === EnterKeyType.Send && !this.ctrlFlag) || (enterKey === EnterKeyType.NEW_LINE && this.ctrlFlag)) {
    this.submit() // 执行发送逻辑
  } else if (enterKey === EnterKeyType.Send && this.ctrlFlag){
    this.controller.addTextSpan('\n'); // 执行换行逻辑
  }
  event.keepEditableState(); // 输入框保持输入态,保证输入连续性
})

3、使用Select组件切换消息发送快捷键:

@State enterTypes: Array<EnterKeyType> = [EnterKeyType.Send, EnterKeyType.NEW_LINE]
@State switchText: string = 'Enter'
@State switchIndex: number = 0;

Select([{ value: 'Enter' }, { value: 'Ctrl + Enter' }])
  .selected(this.switchIndex) // 使用switchIndex作为当前选择模式的索引
  .font({ size: 16, weight: 500 })
  .value(this.switchText)
  .fontColor('#182431')
  .selectedOptionFont({ size: 16, weight: 400 })
  .optionFont({ size: 16, weight: 400 })
  .menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
  .optionWidth(200)
  .optionHeight(300)
  .onSelect((index: number, text?: string | undefined) => {
    console.info('Select:' + index)
    this.switchIndex = index;
    this.switchText = text as string;
  })

4、使用Button组件作为发送消息的按钮:

Button('Submit').onClick(() => {
  // submit为执行发送消息的逻辑处理 
  this.submit();
})

规范标准

按键事件数据流

按键事件由外设键盘等设备触发,经驱动和多模处理转换后发送给当前获焦的窗口,窗口获取到事件后,会尝试分发三次事件。三次分发的优先顺序如下,一旦事件被消费,则跳过后续分发流程。

1、首先分发给ArkUI框架用于触发获焦组件绑定的onKeyPreIme回调和页面快捷键。

2、再向输入法分发,输入法会消费按键用作输入。

3、再次将事件发给ArkUI框架,用于响应系统默认Key事件(例如走焦),以及获焦组件绑定的onKeyEvent回调。

因此,当某输入框组件获焦,且打开了输入法,此时大部分按键事件均会被输入法消费。例如字母键会被输入法用来向输入框中输入对应字母字符、方向键会被输入法用来切换选中备选词。如果在此基础上给输入框组件绑定了快捷键,那么快捷键会优先响应事件,事件也不再会被输入法消费。

按键事件到ArkUI框架之后,会先找到完整的父子节点获焦链。从叶子节点到根节点,逐一发送按键事件。

效果示例

参考Demo:

@Entry
@Component
struct Index {
  @State text: string = '';
  @State enterTypes: Array<EnterKeyType> = [EnterKeyType.Send, EnterKeyType.NEW_LINE];
  @State switchText: string = 'Enter';
  @State switchIndex: number = 0;
  @State ctrlFlag: boolean = false; // 维护ctrl按键状态
  controller: RichEditorController = new RichEditorController();

  build() {
    Column({ space: 20 }) {
      Row({ space: 20 }) {
        Text(this.text)
          .fontSize(16)
          .backgroundColor(0xEEEEEE)
          .margin({ top: 20, right: 20, bottom: 20, left: 20 })
          .border({
            width: 1,
            color: Color.Black,
            style: BorderStyle.Solid
          })
          .width(640).height(360).align(Alignment.TopStart)
      }
      .margin({ top: 20})
      Row({ space: 20 }) {
        RichEditor({ controller: this.controller })
          .margin(10)
          .border({
            width: 1,
            color: Color.Black,
            style: BorderStyle.Solid
          })
          .width(640).height(120).align(Alignment.TopStart)
          .backgroundColor('#FFFFFFFF')
          .enterKeyType(this.enterTypes[this.switchIndex]) // 通过switchIndex切换当前组件的enterKeyType
          .onSubmit((enterKey: EnterKeyType, event: SubmitEvent) => {
            console.info('onSubmit', enterKey);
            // 通过enterKeyType和ctrl状态判断当前执行发送还是换行逻辑
            if ((enterKey === EnterKeyType.Send && !this.ctrlFlag) || (enterKey === EnterKeyType.NEW_LINE && this.ctrlFlag)) {
              this.submit(); // 执行发送逻辑
            } else if (enterKey === EnterKeyType.Send && this.ctrlFlag){
              this.controller.addTextSpan('\n'); // 执行换行逻辑
            }
            event.keepEditableState(); // 输入框保持输入态,保证连续连续性
          })
          .onKeyEvent((event: KeyEvent) => {
            if (event.getModifierKeyState) {
              // 每次keyEvent更新ctrl按键状态
              this.ctrlFlag = event.getModifierKeyState(['Ctrl']);
            }
          })
      }
      Row({ space: 20 }) {
        Button('Submit').onClick(() => {
          this.submit();
        })
        Select([{ value: 'Enter' }, { value: 'Ctrl + Enter' }])
          .selected(this.switchIndex) // 使用switchIndex作为当前选择模式的索引
          .font({ size: 16, weight: 500 })
          .value(this.switchText)
          .fontColor('#182431')
          .selectedOptionFont({ size: 16, weight: 400 })
          .optionFont({ size: 16, weight: 400 })
          .menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
          .optionWidth(200)
          .optionHeight(300)
          .onSelect((index: number, text?: string | undefined) => {
            console.info('Select:' + index)
            this.switchIndex = index;
            this.switchText = text as string;
          })
      }
    }
    .height('100%')
    .width('100%')
  }

  submit() {
    console.info('submit message.');
    let spans = this.controller.getSpans();
    for (let spansElement of spans) {
      if (spansElement) {
        this.text += (spansElement as RichEditorTextSpanResult).value;
      }
    }
    if (!this.text.endsWith('\n')) {
      this.text += '\n'
    }
    this.controller.deleteSpans();
  }
}

参考链接:按键事件数据流按键事件

二、通过快捷键后台唤起场景适配

场景介绍

IM类应用在后台运行时,通常需要使用快捷键拉起程序主界面,由于当前程序不在前台,无法通过键盘事件监听和组件快捷键实现此功能,需要使用系统提供的组合按键订阅模块实现此功能。

导入模块

import { inputConsumer } from '@kit.InputKit'; 

接口说明

接口名称

描述

getAllSystemHotkeys(): Promise<Array<hotkeyoptions>></hotkeyoptions>

获取系统所有快捷键,使用Promise异步回调。

on(type: 'hotkeyChange', hotkeyOptions: HotkeyOptions, callback: Callback<hotkeyoptions>): void</hotkeyoptions>

订阅全局组合按键,当满足条件的组合按键输入事件发生时,使用Callback异步方式上报组合按键数据。

off(type: 'hotkeyChange', hotkeyOptions: HotkeyOptions, callback?: Callback<hotkeyoptions>): void</hotkeyoptions>

取消订阅全局组合按键。

组合按键订阅模块接口如下表所示,接口详细介绍请参考@ohos.multimodalInput.inputConsumer (全局快捷键)

 

订阅全局组合按键

开发步骤

1、定义响应后台唤起的组合事件。

2、定义后台唤起的回调事件。

3、调用接口注册组合事件,判断是否注册成功。

// 1. 定义响应后台唤起的组合事件,此处以 Ctrl + Alt + Z 为例
weekUpKeyOptions: inputConsumer.HotkeyOptions = {
  preKeys: [ KeyCode.KEYCODE_CTRL_LEFT, KeyCode.KEYCODE_ALT_LEFT ],
  finalKey: KeyCode.KEYCODE_Z,
};

// 2. 定义后台唤起的回调事件,拉起当前窗口
weekUpCallback = (keyOptions: inputConsumer.HotkeyOptions) => {
  this.mainWindow.restore();
}

// 3. 注册后台唤起的组合事件
registerWeekUp(): void {
  try {
    inputConsumer.on("hotkeyChange", weekUpKeyOptions, this.weekUpCallback);
    this.weekUpFlag = true;
  } catch (error) {
    this.weekUpFlag = false;
    console.log(`>>>>>>>>>>Subscribe failed, Cause code: ${error.code}, message: ${error.message}`);
  }
}

取消订阅全局组合按键

开发步骤

1、使用定义好的组合事件。

2、调用接口取消注册组合事件。

unregisterWeekUp(): void {
  try {
    inputConsumer.off("hotkeyChange", weekUpKeyOptions);
    this.weekUpFlag = false;
  } catch (error) {
    this.weekUpFlag = true;
    console.error(`>>>>>>>>>>Execute failed, error: ${JSON.stringify(error, [`code`, `message`])}`);
  }
}

三、快捷键截图场景适配

IM类应用在运行时,通常需要使用快捷键截图,由于当前程序不在前台,无法通过键盘事件监听和组件快捷键实现此功能,需要使用系统提供的组合按键订阅模块实现此功能,实现方式与后台唤起相同,将后台唤起回调逻辑换成截图逻辑即可。

参考示例

import { inputConsumer } from '@kit.InputKit';
import { KeyCode } from '@kit.InputKit';
import { screenshot, window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// 1. 定义响应后台唤起的组合事件,此处以 Ctrl + Alt + Z 为例
const weekUpKeyOptions: inputConsumer.HotkeyOptions = {
  preKeys: [ KeyCode.KEYCODE_CTRL_LEFT, KeyCode.KEYCODE_ALT_LEFT ],
  finalKey: KeyCode.KEYCODE_Z,
};

const screenshotKeyOptions: inputConsumer.HotkeyOptions = {
  preKeys: [ KeyCode.KEYCODE_CTRL_LEFT, KeyCode.KEYCODE_ALT_LEFT ],
  finalKey: KeyCode.KEYCODE_A,
};

@Entry
@Component
struct Index {
  @State text: string = '';
  @State enterTypes: Array<EnterKeyType> = [EnterKeyType.Send, EnterKeyType.NEW_LINE];
  @State switchText: string = 'Enter';
  @State switchIndex: number = 0;
  @State ctrlFlag: boolean = false; // 维护ctrl按键状态
  @State weekUpFlag: boolean = false;
  @State screenshotFlag: boolean = false;
  private controller: RichEditorController = new RichEditorController();
  private mainWindow?: window.Window;

  aboutToAppear() {
    this.registerWeekUp();
    this.registerScreenshot();
    window.getLastWindow(this.getUIContext().getHostContext()).then((mainWindow: window.Window) => {
      this.mainWindow = mainWindow;
    });
  }

  // 2. 定义后台唤起的回调事件,拉起当前窗口
  weekUpCallback = (keyOptions: inputConsumer.HotkeyOptions) => {
    this.mainWindow?.restore();
  }

  // 3. 注册后台唤起的组合事件
  registerWeekUp(): void {
    try {
      inputConsumer.on("hotkeyChange", weekUpKeyOptions, this.weekUpCallback);
      this.weekUpFlag = true;
    } catch (error) {
      this.weekUpFlag = false;
      console.log(`>>>>>>>>>>Subscribe failed, Cause code: ${error.code}, message: ${error.message}`);
    }
  }

  unregisterWeekUp(): void {
    try {
      inputConsumer.off("hotkeyChange", weekUpKeyOptions);
      this.weekUpFlag = false;
    } catch (error) {
      this.weekUpFlag = true;
      console.error(`>>>>>>>>>>Execute failed, error: ${JSON.stringify(error, [`code`, `message`])}`);
    }
  }

  screenshotCallback = (keyOptions: inputConsumer.HotkeyOptions) => {
    try {
      screenshot.pick().then((pickInfo: screenshot.PickInfo) => {
        console.log('pick Pixel bytes number: ' + pickInfo.pixelMap.getPixelBytesNumber());
        console.log('pick Rect: ' + pickInfo.pickRect);
        this.controller.addImageSpan(pickInfo.pixelMap);
        pickInfo.pixelMap.release(); // PixelMap使用完后及时释放内存
      }).catch((err: BusinessError) => {
        console.log('Failed to pick. Code: ' + JSON.stringify(err));
      });
    } catch (exception) {
      console.error('Failed to pick Code: ' + JSON.stringify(exception));
    };
  }

  registerScreenshot(initFlag?: boolean): void {
    try {
      inputConsumer.on("hotkeyChange", screenshotKeyOptions, this.screenshotCallback);
      this.screenshotFlag = true;
    } catch (error) {
      this.screenshotFlag = false;
      console.log(`>>>>>>>>>>Subscribe failed, Cause code: ${error.code}, message: ${error.message}`);
    }
  }

  unregisterScreenshot(): void {
    try {
      inputConsumer.off("hotkeyChange", screenshotKeyOptions);
      this.screenshotFlag = false;
    } catch (error) {
      this.screenshotFlag = true;
      console.error(`>>>>>>>>>>Execute failed, error: ${JSON.stringify(error, [`code`, `message`])}`);
    }
  }

  build() {
    Column({ space: 20 }) {
      Row({ space: 20 }) {
        Text(this.text)
          .fontSize(16)
          .backgroundColor(0xEEEEEE)
          .margin({ top: 20, right: 20, bottom: 20, left: 20 })
          .border({
            width: 1,
            color: Color.Black,
            style: BorderStyle.Solid
          })
          .width(640).height(360).align(Alignment.TopStart)
      }
      .margin({ top: 20})
      Row({ space: 20 }) {
        RichEditor({ controller: this.controller })
          .margin(10)
          .border({
            width: 1,
            color: Color.Black,
            style: BorderStyle.Solid
          })
          .width(640).height(120).align(Alignment.TopStart)
          .backgroundColor('#FFFFFFFF')
          .enterKeyType(this.enterTypes[this.switchIndex]) // 通过switchIndex切换当前组件的enterKeyType
          .onSubmit((enterKey: EnterKeyType, event: SubmitEvent) => {
            console.info('onSubmit', enterKey);
            // 通过enterKeyType和ctrl状态判断当前执行发送还是换行逻辑
            if ((enterKey === EnterKeyType.Send && !this.ctrlFlag) || (enterKey === EnterKeyType.NEW_LINE && this.ctrlFlag)) {
              this.submit(); // 执行发送逻辑
            } else if (enterKey === EnterKeyType.Send && this.ctrlFlag){
              this.controller.addTextSpan('\n'); // 执行换行逻辑
            }
            event.keepEditableState(); // 输入框保持输入态,保证连续连续性
          })
          .onKeyEvent((event: KeyEvent) => {
            if (event.getModifierKeyState) {
              // 每次keyEvent更新ctrl按键状态
              this.ctrlFlag = event.getModifierKeyState(['Ctrl']);
            }
          })
      }
      Row({ space: 20 }) {
        Button('Submit').onClick(() => {
          this.submit();
        })
        Select([{ value: 'Enter' }, { value: 'Ctrl + Enter' }])
          .selected(this.switchIndex) // 使用switchIndex作为当前选择模式的索引
          .font({ size: 16, weight: 500 })
          .value(this.switchText)
          .fontColor('#182431')
          .selectedOptionFont({ size: 16, weight: 400 })
          .optionFont({ size: 16, weight: 400 })
          .menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
          .optionWidth(200)
          .optionHeight(300)
          .onSelect((index: number, text?: string | undefined) => {
            console.info('Select:' + index)
            this.switchIndex = index;
            this.switchText = text as string;
          })
      }
      Row({ space: 20 }) {
        Text('后台唤起快捷键(Ctrl+Alt+Z):')
        Toggle({ type: ToggleType.Switch, isOn: this.weekUpFlag })
          .selectedColor('#FF007DFF')
          .switchPointColor('#FFFFFFFF')
          .onChange((isOn: boolean) => {
            if (isOn) {
              this.registerWeekUp();
            } else {
              this.unregisterWeekUp();
            }
          })
        Text('截图快捷键(Ctrl+Alt+A):')
        Toggle({ type: ToggleType.Switch, isOn: this.screenshotFlag })
          .selectedColor('#FF007DFF')
          .switchPointColor('#FFFFFFFF')
          .onChange((isOn: boolean) => {
            if (isOn) {
              this.registerScreenshot();
            } else {
              this.unregisterScreenshot();
            }
          })
      }
    }
    .height('100%')
    .width('100%')
  }

  submit() {
    console.info('submit message.');
    let spans = this.controller.getSpans();
    for (let spansElement of spans) {
      if (spansElement) {
        this.text += (spansElement as RichEditorTextSpanResult).value;
      }
    }
    if (!this.text.endsWith('\n')) {
      this.text += '\n'
    }
    this.controller.deleteSpans();
  }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐