背景

软键盘是用户进行交互的重要途径之一,但软键盘的弹出和收起,可能会影响到正在显示的 UI 元素,影响用户体验。

常见的问题有:

  • 重要信息被软键盘遮挡:当软键盘弹出时,输入框或其它重要 UI 元素可能会被键盘遮挡
  • 软键盘弹出导致布局错位:内容可能会不恰当上移,影响用户体验
  • 软键盘弹出导致弹窗过度上抬:弹窗被键盘上顶,造成不好的体验

这篇文章聊聊键盘适配的那些事儿,包括系统键盘的避让机制、自定义键盘的实现,以及一些常见问题的解决方法。

本文是上篇,主要介绍系统键盘基础、软键盘避让机制和常见问题解决方案。下篇将介绍自定义键盘的实现。


系统键盘基础

软键盘的弹出和收起

用户点击输入框时,软键盘默认弹出。但在特定场景下,需要对软键盘的弹出和收起进行控制。

主动获焦弹出软键盘

有时候进入页面,希望页面中的输入框能主动获焦并且弹出软键盘,方便用户直接输入。

可以通过将输入框的 defaultFocus 设置为 true 来实现:

TextInput()
  .defaultFocus(true)
代码控制弹出软键盘

开发者可以使用 FocusController 的 requestFocus 方法,通过组件的 id 将焦点转移到组件树对应的实体节点,并且弹出软键盘。

例如,表情面板切换到文本输入时,点击表情图标拉起系统软键盘:

TextInput({ placeholder: 'Please enter a contact name' })
  .id('input1')

Button('login')
  .onClick(() => {
    this.getUIContext().getFocusController().requestFocus('input1');
  })

注意:使用 requestFocus 需要保证 TextInput 组件已经挂载完成,应避免在组件未创建的情况下使用。

代码控制收起软键盘

通过全局的焦点控制对象 FocusController 的 clearFocus 方法,软键盘收起。

例如在搜索页面中,点击搜索按钮时软键盘收起:

Button('Search')
  .onClick(() => {
    this.getUIContext().getFocusController().clearFocus();
  })

此外,开发者可调用 stopEditing 方法来关闭键盘,该方法需为输入框单独绑定一个 TextInputController 对象。在存在多个输入框的场景下,需要绑定多个 TextInputController 对象,使用起来较为繁琐,推荐改用 clearFocus 方法来解除焦点。

监听软键盘高度

开发者可以通过获取软键盘高度、监听软键盘的弹出和收起状态,调整组件位置以适配界面或显示隐藏某些组件。

通过 window 模块的 on(‘keyboardHeightChange’) 方法开启软键盘高度变化的监听,实时获取软键盘高度。

下面这个示例,软键盘弹起后显示表情栏,软键盘收起后隐藏表情栏:

import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct GetKeyboardHeightDemo {
  @State keyboardHeight: number = 0;

  aboutToAppear(): void {
    try {
      window.getLastWindow(this.getUIContext().getHostContext()).then(currentWindow => {
        currentWindow.on('keyboardHeightChange', (data: number) => {
          this.keyboardHeight = this.getUIContext().px2vp(data);
        })
      })
    } catch (error) {
      let err = error as BusinessError;
      hilog.error(0x0000, 'GetKeyboardHeightDemo',
        `getLastWindow failed, error code=${err.code}, message=${err.message}`);
    }
  }

  build() {
    Column() {
      TextInput()

      if (this.keyboardHeight > 0) {
        Row() {
          // Emoji
        }
      }
    }
  }
}

当 keyboardHeight 为 0 的时候表示软键盘处于收起状态,此时隐藏表情栏;keyboardHeight 不为 0 的时候表示软键盘处于弹出状态,此时显示表情栏。

监听安全区域高度

通过 window 模块的 on(‘avoidAreaChange’) 方法开启当前窗口系统规避区变化的监听,获取内容可视区域大小,同时也可以监听软键盘的弹出收起。

根据软键盘弹出后的可视区域大小,动态调整布局中组件的高度以适配界面:

import { KeyboardAvoidMode, window } from '@kit.ArkUI';
import { UIAbility } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct GetSafeAreaHeightDemo {
  @State screenHeight: number = 0;
  @State isKeyBoardHidden: boolean = false;

  aboutToAppear(): void {
    try {
      window.getLastWindow(this.getUIContext().getHostContext()).then(currentWindow => {
        let property = currentWindow.getWindowProperties();
        let avoidArea = currentWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_KEYBOARD);
        
        this.screenHeight = this.getUIContext()
          .px2vp(property.windowRect.height - avoidArea.topRect.height - avoidArea.bottomRect.height);
        
        currentWindow.on('avoidAreaChange', data => {
          if (data.type !== window.AvoidAreaType.TYPE_KEYBOARD) {
            return;
          }
          if (data.area.bottomRect.height <= 0) {
            this.isKeyBoardHidden = true;
          } else {
            this.isKeyBoardHidden = false;
          }
          this.screenHeight = this.getUIContext()
            .px2vp(property.windowRect.height - data.area.topRect.height - data.area.bottomRect.height);
        })
      })
    } catch (error) {
      let err = error as BusinessError;
      hilog.error(0x0000, 'GetSafeAreaHeightDemo',
        `getLastWindow failed, error code=${err.code}, message=${err.message}`);
    }
  }

  build() {
    Column() {
      TextInput()
    }
  }
}

软键盘避让机制

解决软键盘的界面适配问题,首先需要了解在 HarmonyOS 系统中软键盘的避让机制。

默认避让效果

为了确保输入框不被软键盘挡住,系统默认提供了输入框避让软键盘的能力。

默认情况下,系统针对输入框位置,执行安全避让策略:

  • 如果当前输入框不会被软键盘遮挡,则不上抬组件
  • 当前输入框会被软键盘遮挡,则上抬组件至刚好在软键盘上方显示完整的输入框,输入框上方的组件会跟着抬起,下方的组件不会露出

软键盘避让模式

当用户在输入时,为了确保输入框不会被键盘遮挡,系统提供了避让模式来解决这一问题。

开发者可以通过 setKeyboardAvoidMode 控制虚拟键盘抬起时页面的避让模式,键盘抬起时默认页面避让模式为上抬模式。

上抬模式(KeyboardAvoidMode.OFFSET)

为了避让软键盘,Page 内容会整体上抬。

import { KeyboardAvoidMode, window } from '@kit.ArkUI';
import { UIAbility } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/GetSafeAreaHeightDemo', (err) => {
      windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET);
    });
  }
}
压缩模式(KeyboardAvoidMode.RESIZE)

当软键盘高度改变时,调整 Page 大小。Page 下设置百分比宽高的组件会跟随压缩,直接设置宽高的组件保持固定大小。

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/GetSafeAreaHeightDemo', (err) => {
      windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
    });
  }
}

设置 KeyboardAvoidMode.RESIZE 时,expandSafeArea([SafeAreaType.KEYBOARD],[SafeAreaEdge.BOTTOM]) 不生效。

不避让模式(KeyboardAvoidMode.NONE)

软键盘将直接覆盖页面 UI,不会触发界面布局调整。

例如在全屏沉浸式场景(游戏/视频播放器等),为保障用户体验的完整性,开发者可以使用 KeyboardAvoidMode.NONE 模式:

aboutToAppear(): void {
  this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.NONE);
}
光标避让

上抬模式和压缩模式还包括 KeyboardAvoidMode.OFFSET_WITH_CARET 和 KeyboardAvoidMode.RESIZE_WITH_CARET 两种。

当输入框的光标位置发生变化时,系统会自动触发相应的界面避让行为,确保光标始终处于可视区域内。

弹窗类组件避让模式

弹窗类组件(如 Dialog、Popup、Menu、BindSheet 等)避让模式有 KeyboardAvoidMode.DEFAULT(避让)和 KeyboardAvoidMode.NONE(不避让)两种。

通过 BaseDialogOptions 中的 keyboardAvoidMode 属性,灵活控制是否避让软键盘:

this.getUIContext().getPromptAction().openCustomDialog({
  builder: () => {
    this.customDialogBuilder();
  },
  alignment: DialogAlignment.Bottom,
  width: '100%',
  keyboardAvoidMode: KeyboardAvoidMode.NONE,
});

若未指定弹窗避让模式,则其避让行为受页面避让模式影响。

设置组件不避让软键盘

前面介绍了避让模式,组件会为了避让软键盘而移动。有时希望组件不避让软键盘,例如在上抬模式下,希望顶部标题栏不移动。

通过 expandSafeArea 属性支持组件不改变布局情况下扩展其绘制区域至安全区外。

当设置 expandSafeArea 属性 type 为 SafeAreaType.KEYBOARD 的时候,即 expandSafeArea([SafeAreaType.KEYBOARD]),系统会将软键盘区域视作安全区,从而不会避让软键盘。


常见问题与解决方案

重要信息被软键盘遮挡

例如下面这个电子邮件示例,内容包括标题栏、内容区域和底部操作栏。点击输入内容的输入框时,软键盘会遮挡底部操作栏。

其中标题栏和底部操作栏都是固定的高度 56,内容区域高度是非固定高度 layoutWeight(1),自适应高度。

@Component
export struct MailPage {
  build() {
    Column() {
      this.NavigationTitle()
      this.EmailContent()
      this.BottomToolbar()
    }
  }

  @Builder
  NavigationTitle() {
    Row() {
      // ...
    }
    .width('100%')
    .height("56vp")
  }

  @Builder
  BottomToolbar() {
    Row({ space: 12 }) {
      // ...
    }
    .width('100%')
    .height("56vp")
  }

  @Builder
  EmailContent() {
    Column() {
      // ...
    }
    .width('100%')
    .layoutWeight(1)
  }
}

开发者可以通过设置软键盘的避让模式为 KeyboardAvoidMode.RESIZE(压缩模式),来解决底部操作栏被遮挡的问题。

设置该属性后,软键盘的避让会通过压缩内容区域的高度来实现:

aboutToAppear(): void {
  this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
}

aboutToDisappear(): void {
  this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET);
}

需要注意的是内容区域高度的设置需要用百分比的方式实现。

软键盘弹出导致布局错位

顶部固定,内容区域上抬

例如下面的一个聊天界面,顶部是一个自定义的标题,下方为可滚动聊天消息区域,底部是消息输入框。

由于软键盘避让默认为上抬模式,会将整个页面向上抬起,因此标题也会被顶上去。

现在需求是希望顶部标题固定,点击底部输入框软键盘弹起的时候,标题不上抬,只有内容区域上抬。

想要顶部标题不被软键盘向上抬,需要给对应的组件设置 expandSafeArea([SafeAreaType.KEYBOARD]) 属性,使标题组件不避让键盘:

@Entry
@Component
export struct ContactPage {
  build() {
    Row() {
      Column() {
        Row() {
          // ...
        }
        .height('12%')
        .expandSafeArea([SafeAreaType.KEYBOARD])
        .zIndex(1)

        List() {
          // ...
        }
        .height('76%')

        Column() {
          // ...
        }
        .height('12%')
      }
      .width('100%')
    }
    .height('100%')
  }
}

软键盘弹出导致弹窗过度上抬

自定义弹窗被键盘顶起,影响用户体验。

在软键盘系统避让机制中介绍过,弹窗为避让软键盘会整体向上抬,这样可能会影响用户体验。

比如下面这个评论列表的弹窗,使用@CustomDialog 实现的:

@CustomDialog
struct CommentDialog {
  listData: string[] = ['comments1', 'comments2', 'comments3', 'comments4', 'comments5', 'comments6', 'comments7', 'comments8'];

  build() {
    Column() {
      Text('comments')
        .fontSize(20)
        .fontWeight(FontWeight.Medium)

      List() {
        ForEach(this.listData, (item: string) => {
          ListItem() {
            Text(item)
              .height(80)
              .fontSize(20)
          }
        }, (item: string) => item)
      }
      .scrollBar(BarState.Off)
      .width('100%')
      .layoutWeight(1)

      TextInput({ placeholder: 'Please input content' })
        .height(40)
        .width('100%')
    }
    .padding(12)
  }
}

@Entry
@Component
struct CustomDialogDemo {
  dialogController: CustomDialogController | null = new CustomDialogController({
    builder: CommentDialog(),
    alignment: DialogAlignment.Bottom,
    cornerRadius: 0,
    width: '100%',
    height: '80%'
  })

  build() {
    Column() {
      Button('click me')
        .onClick(() => {
          if (this.dialogController !== null) {
            this.dialogController.open();
          }
        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

当用户点击弹窗底部的输入框的时候,弹窗会整体上抬,输入框上抬的距离也过多。

为了解决以上问题,可以使用 Navigation.Dialog,通过设置 NavDestination 的 mode 为 NavDestinationMode.DIALOG 弹窗类型,此时整个 NavDestination 默认透明显示:

@Entry
@Component
struct NavDestinationModeDemo {
  @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()

  @Builder
  PagesMap(name: string) {
    if (name === 'DialogPage') {
      DialogPage()
    }
  }

  build() {
    Navigation(this.pageStack) {
      Column() {
        Button('click me')
          .onClick(() => {
            this.pageStack.pushPathByName('DialogPage', '');
          })
      }
      .height('100%')
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .mode(NavigationMode.Stack)
    .navDestination(this.PagesMap)
  }
}

@Component
export struct DialogPage {
  @Consume('NavPathStack') pageStack: NavPathStack;
  listData: string[] = ['评论 1', '评论 2', '评论 3', '评论 4', '评论 5', '评论 6', '评论 7', '评论 8'];

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Bottom }) {
        Column() {
          Text('评论')
            .fontSize(20)
            .fontWeight(FontWeight.Medium)

          List() {
            ForEach(this.listData, (item: string) => {
              ListItem() {
                Text(item)
                  .height(80)
                  .fontSize(20)
              }
            }, (item: string) => item)
          }
          .scrollBar(BarState.Off)
          .width('100%')
          .layoutWeight(1)

          TextInput({ placeholder: 'Please input content' })
            .height(40)
            .width('100%')
        }
        .backgroundColor(Color.White)
        .height('75%')
        .width('100%')
        .padding(12)
      }
      .height('100%')
      .width('100%')
    }
    .backgroundColor('rgba(0,0,0,0.2)')
    .hideTitleBar(true)
    .mode(NavDestinationMode.DIALOG)
  }
}

还需设置软键盘避让模式为压缩模式:

import { UIAbility } from "@kit.AbilityKit";
import { window, KeyboardAvoidMode } from "@kit.ArkUI";

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/GetSafeAreaHeightDemo', (err) => {
      windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
    });
  }
}

运行效果如下,点击输入框后,内容区域会进行压缩,弹窗整体不会发生上抬。

设置软键盘和弹窗组件距离

弹窗类组件默认避让模式下,软键盘弹起之后弹窗组件之间 16vp 间隔。

开发者可以通过弹窗参数 BaseDialogOptions 中 keyboardAvoidDistance,调整弹窗组件与软键盘之间的避让距离。

设置软键盘间距时,需要将 keyboardAvoidMode 值设为 KeyboardAvoidMode.DEFAULT:

this.getUIContext().getPromptAction().openCustomDialog({
  builder: () => {
    this.customDialogBuilder();
  },
  alignment: DialogAlignment.Bottom,
  width: '100%',
  keyboardAvoidDistance: LengthMetrics.vp(0)
});

上篇总结

上篇介绍了系统键盘的基础知识和避让机制:

系统键盘基础

  • 软键盘的弹出和收起控制(defaultFocus、requestFocus、clearFocus)
  • 监听软键盘高度(keyboardHeightChange)
  • 监听安全区域高度(avoidAreaChange)

软键盘避让机制

  • 默认避让效果
  • 三种避让模式(上抬/压缩/不避让)
  • 弹窗类组件避让模式
  • 设置组件不避让软键盘(expandSafeArea)

常见问题解决方案

  • 重要信息被软键盘遮挡(使用压缩模式)
  • 软键盘弹出导致布局错位(使用 expandSafeArea 固定顶部)
  • 软键盘弹出导致弹窗过度上抬(使用 Navigation.Dialog)
  • 设置软键盘和弹窗组件距离(keyboardAvoidDistance)

下篇预告:自定义键盘实现,包括自定义键盘布局、输入控制、光标控制、布局避让等。

Logo

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

更多推荐