目录

多设备交互

概述

在开发“一多”应用时,除了需要针对手机、平板、电脑、穿戴设备、智慧屏等设备的硬件差异进行适配外,还应关注其在交互方式上的差异性。不同设备的用户交互依赖于各异的输入设备,从而形成不同的交互方式,例如:手机/平板通过手指触控屏幕、电脑通过鼠标控制光标、智慧屏通过灵犀指向遥控控制光标。为确保应用在各类设备上均具备自然流畅的可用性,开发时需系统性关注并差异化适配相应的交互事件。

多设备交互

多设备交互是指应用在不同设备上为实现同一功能,适配相应的输入设备进行交互操作。例如,在视频类应用中控制视频播放/暂停时,不同设备的交互形式各异:

多设备交互开发需针对触屏、鼠标、键盘、遥控器等不同输入设备进行交互适配,在保证功能一致性的基础上,遵循各设备的交互规范,实现自然流畅的应用体验。

交互归一

交互归一是一种面向多设备输入的响应框架,通过将不同输入设备的交互行为抽象为同一事件,来简化开发逻辑,例如:触屏点击、触控板点击、鼠标左键单击、遥控器OK键确认等统一抽象为点击事件;遥控器功能键、键盘快捷键抽象为按键事件。交互归一实现对多样化输入源的统一处理,确保组件在不同交互场景下具备一致的行为逻辑与用户体验。

图1 输入设备

交互归一并非将所有输入方式简单合并为单一事件或通过一个API处理,而是通过对不同设备的几十种底层交互事件进行语义抽象与归类,在保证交互差异可控的前提下,大幅减少事件类型数量。最终形成的是一组标准化的交互API集合,开发者仍需根据具体场景选择并适配相应的抽象事件,以实现跨设备的一致性与灵活性兼顾的交互体验。

ArkUI框架提供了丰富的交互功能,支持直接处理基础输入事件,以及由这些事件驱动的手势事件,同时支持拖拽事件、焦点事件等复杂交互。

[h2]基础输入事件

当用户使用输入设备(如触摸屏、键盘、鼠标或触控板)进行交互时,设备驱动层会检测并生成相应的原始信号。操作系统捕获这些底层信号,并将其封装为标准化的“基础事件”,随后传递给上层应用程序进行处理。

根据基础事件特点总体上分为两类,指向性与非指向性事件。从“事件的目标如何确定”来理解指向性事件和非指向性事件:

[h2]手势事件

手势是由一系列基础事件累积并满足特定条件后被识别出的交互行为,例如“点击”即为快速按下并抬起。

在ArkUI中,系统组件默认支持常见手势(如按钮支持点击事件),也可在组件上绑定一个或多个手势。默认情况下,多手势识别会按照手势注册的顺序依次进行匹配和处理:

下图为常见手势事件在不同输入设备上的触发方式。

图4 输入设备手势事件触发方式一览表

如下是旋转手势的示例代码:

.gesture(
  RotationGesture()
    .onActionUpdate((event: GestureEvent) => {
      if (event) {
        // Obtain the rotation angle and change the rotation angle of the image
        this.angle = this.rotateValue + event.angle;
        this.sourceType = event.source;
        this.sourceTool = event.sourceTool;
        this.getUserTextData();
      }
    })
    .onActionEnd(() => {
      this.rotateValue = this.angle;
    })
)

典型场景举例:宫格排列的界面元素,可以通过双指触发PinchGesture捏合手势,动态修改网格布局的columnsTemplate属性实现。如视频应用中双指捏合实现视频元素的显示个数。

效果图如下:

md

lg

Grid() {
  // ...
}
// ...
.gesture(PinchGesture({ fingers: 2 }).onActionUpdate((event: GestureEvent) => {
  if (event.scale > 1 && this.currentWidthBreakpoint !== 'sm') {
    if (this.currentWidthBreakpoint === 'md') {
      this.getUIContext().animateTo({
        duration: 500
      }, () => {
        this.videoGridColumn = '1fr 1fr 1fr';
      })
    } else {
      this.getUIContext().animateTo({
        duration: 500
      }, () => {
        this.videoGridColumn = '1fr 1fr 1fr 1fr';
      })
    }
  } else if (event.scale < 1 && this.currentWidthBreakpoint !== 'sm') {
    if (this.currentWidthBreakpoint === 'md') {
      this.getUIContext().animateTo({
        duration: 500
      }, () => {
        this.videoGridColumn = '1fr 1fr 1fr 1fr';
      })
    } else {
      this.getUIContext().animateTo({
        duration: 500
      }, () => {
        this.videoGridColumn = '1fr 1fr 1fr 1fr 1fr';
      })
    }
  } else {
    Logger.info(`Two-finger operation is not supported`);
  }
}))

[h2]焦点事件

使用键盘、电视遥控器、车机摇杆或旋钮等非指向性输入设备与应用程序进行间接交互时,建议将页面中可操作元素设置为可获焦状态,并配置获焦视觉效果,以保证交互体验。

图6 走焦样式指引案例

更多焦点事件的能力和规范请参考支持焦点处理。

表1 走焦常用属性

走焦常用属性

参数

描述

focusable

boolean

设置当前组件是否可以获焦

tabIndex

number

自定义组件Tab键走焦顺序值

defaultFocus

boolean

设置当前组件是否为当前页面上的默认焦点

tabStop

boolean

设置当前容器组件是否为走焦可停留容器

nextFocus

Optional<FocusMovement>

设置当前容器组件的自定义走焦规则

下图为不同输入设备上支持触发焦点事件的方式。

图7 输入设备焦点事件触发方式一览表

[h2]拖拽事件

拖拽发生在两个组件之间,它不是简单的单次输入,而是一个”过程”,通常包含如下步骤(以将组件A拖拽到组件B中为例)。

一个完整的拖拽事件,包含多个拖拽子事件,如下表所示。

表2 拖拽方法

名称

功能描述

onDragStart

绑定A组件,触控屏长按/鼠标左键按下后移动触发

onDragEnter

绑定B组件,触控屏手指、鼠标移动进入B组件瞬间触发

onDragMove

绑定B组件,触控屏手指、鼠标在B组件内移动触发

onDragLeave

绑定B组件,触控屏手指、鼠标移动退出B组件瞬间触发

onDrop

绑定B组件,在B组件内,触控屏手指抬起、鼠标左键松开时触发

下图为不同输入设备上触发拖拽事件的方式。

图8 输入设备拖拽事件触发方式一览表

如下是图片拖拽的示例代码:

.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop((event: DragEvent) => {
  try {
    let dragData: UnifiedData = (event as DragEvent).getData() as UnifiedData;
    if (dragData !== undefined) {
      let records: unifiedDataChannel.UnifiedRecord[] = dragData.getRecords();
      if (records.length > 0) {
        for (let i = 0; i < records.length; i++) {
          let types = records[i].getTypes();
          if (types.includes(uniformTypeDescriptor.UniformDataType.FILE_URI)) {
            // Retrieve the image resource URLs from the record and assign them
            const fileUriUds =
              records[i].getEntry(uniformTypeDescriptor.UniformDataType.FILE_URI) as uniformDataStruct.FileUri;
            let typeDescriptor = uniformTypeDescriptor.getTypeDescriptor(fileUriUds.fileType);
            if (typeDescriptor.belongsTo(uniformTypeDescriptor.UniformDataType.IMAGE)) {
              this.targetImage = fileUriUds.oriUri;
            }
          }
        }
      } else {
        hilog.info(0x0000, TAG, `%{public}s`, `dragData arr is null`);
      }
    } else {
      hilog.info(0x0000, TAG, `%{public}s`, `dragData is undefined`);
    }
    this.dragSuccess = true;
  } catch (error) {
    const err = error as BusinessError;
    hilog.error(0x0000, TAG, `startDataLoading errorCode: ${err.code}, errorMessage: ${err.message}`);
  }
})

应用开发步骤

在应用开发中,应根据业务场景明确需适配的目标设备。例如,视频类应用通常需覆盖手机、平板、电脑及智慧屏,而运动健康类应用则侧重手机与智能穿戴设备。开发者应结合业务特点和用户高频使用场景,确定适配范围。在明确目标设备后,需进一步依据各设备支持的输入方式(如触控屏、鼠标、键盘、表冠等),针对性地设计和适配相应的交互事件,以保障良好的用户体验。

下表为不同设备支持的输入设备一览表。

表3 设备支持的输入设备一览表

设备

输入设备

直板机

触控屏

折叠屏

触控屏

阔折叠

触控屏、手写笔(Pura X Max)

三折叠

触控屏、手写笔(Mate XTs、Mate XT 2)

平板

触摸屏、鼠标、键盘、手写笔

电脑

触控屏、触控板、鼠标、键盘、手柄

智慧屏

灵犀指向遥控、灵犀悬浮触控、灵犀手写笔、键盘、鼠标

智能穿戴

触控屏、表冠

接下来,以实现视频播放页的暂停/播放功能为例,阐述在多设备交互适配中的开发思路与实施步骤:

  1. 首先,明确长视频类应用的目标适配设备为手机、平板、电脑及智慧屏。
  2. 根据设备支持的输入设备一览表可知手机、平板、电脑和智慧屏支持的输入设备包括:触控屏、触控板(部分设备)、鼠标、键盘、手柄、灵犀指向遥控、灵犀悬浮触控(智慧屏)。
  3. 在实现播放/暂停功能时,需根据不同输入设备的交互方式,适配相应的事件处理机制。根据输入设备手势事件触发方式一览表可知:对于触控屏、鼠标和 灵犀指向遥控,均可通过响应onClick点击事件来触发操作;根据输入设备基础输入事件触发方式一览表可知,使用键盘时,则需监听特定按键(如空格键、回车键)的onKeyEvent按键事件,识别后执行对应逻辑。

    • 触控屏:单击屏幕。鼠标:点击左键。灵犀指向遥控:单击OK键。
      Flex({
        // ...
      }) {
        Column() {
          // ...
      }
      .width('100%')
      .onClick(() => {
        // ...
      })
    • 键盘:键盘按下空格键。空格键的键码为KEYCODE_SPACE,其他按键的键码可通过keyCode枚举查询,例如回车键对应KEYCODE_ENTER。应用可根据实际交互需求,灵活适配不同按键。
      .onKeyEvent((event?: KeyEvent) => {
        //If the key type is pressed, the subsequent code will not be executed,
        // and the specific key logic will be executed when released.
        if (!event || event.type !== KeyType.Down) {
          return;
        }
        // Space key controls pause/play.
        if (event.keyCode === KeyCode.KEYCODE_SPACE) {
          this.avPlayerUtil?.playerStateControl();
        } else if (event.keyCode === KeyCode.KEYCODE_ESCAPE) {
          //Press ESC to exit full screen.
          if (this.isFullScreen) {
            this.exitFullScreen();
          }
        } else if (event.keyCode === KeyCode.KEYCODE_DPAD_RIGHT) {
          //Right-click fast forward
          this.avPlayerUtil?.fastForward();
        } else if (event.keyCode === KeyCode.KEYCODE_DPAD_LEFT) {
          //Left click to go back quickly
          this.avPlayerUtil?.rewind();
        } else {
          return;
        }
      })

开发者可根据业务场景明确适配的设备范围,并针对不同功能所支持的输入方式,灵活选择与之匹配的交互事件进行适配。

示例代码