鸿蒙NEXT开发【软键盘布局适配场景】应用框架开发

概述

软键盘是用户进行交互的重要途径之一,同时软键盘的弹出和收起,可能会影响到正在显示的UI元素,影响用户体验,出现如下常见的软键盘布局适配问题:

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

本文将介绍以下知识帮助开发者了解软键盘的弹出和收起的控制、避让机制以及软键盘常见问题的解决方法。

  • 软键盘的弹出收起和监听
  • 软键盘避让机制
  • 软键盘避让常见问题

软键盘的弹出收起和监听

当用户点击输入框的时候,默认会弹出软键盘。但是某些场景下,需要对软键盘的弹出和收起进行控制。比如点击空白区希望收起软键盘、进入页面的时候希望页面中的输入框能主动获焦并且弹出软键盘等,此外开发者可能需要根据软键盘的弹出和收起状态、软键盘的弹出高度来进行页面布局调整。本节将介绍软键盘的弹出收起控制、获取软键盘高度以及安全区域高度变化监听。

主动获焦弹出软键盘

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

TextInput()
  .defaultFocus(true)

代码控制弹出软键盘

开发者可以使用全局的焦点控制对象FocusController的[requestFocus]方法,通过组件的id将焦点转移到组件树对应的实体节点,并且弹出软键盘。例如下面这个新增地址的示例,当用户未输入信息的时候,点击保存按钮,提示用户输入信息,并且弹出输入框,便于用户直接输入。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例如下:

TextInput({ placeholder: '请输入联系人姓名' })
  .id('input1')

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

代码控制收起软键盘

通过全局的焦点控制对象FocusController的[clearFocus]方法,软键盘收起,例如下面的商品列表页面,点击搜索会收起软键盘。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例代码如下:

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

监听获取软键盘高度

开发者还可以通过获取软键盘高度、监听软键盘的弹出和收起状态,来调整组件的位置来适配界面或者显示隐藏某些组件。通过[window]模块的[on(‘keyboardHeightChange’)]方法开启固定态软键盘高度变化的监听,实时获取软键盘宽高。例如下面这个示例软键盘弹起后显示表情栏,软键盘收起后隐藏表情栏。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面效果图的实现示例代码如下,通过[on(‘keyboardHeightChange’)]方法实时获取软键盘高度赋值给变量keyboardHeight,当keyboardHeight为0的时候表示软键盘处于收起状态,此时隐藏表情栏;keyboardHeight不为0的时候表示软键盘处于弹出状态,此时显示表情栏。

import { window } from '@kit.ArkUI';

@Entry
@Component
struct GetKeyboardHeightDemo {
  @State keyboardHeight: number = 0; // 软键盘高度

  aboutToAppear(): void {
    window.getLastWindow(getContext(this)).then(currentWindow => {
      currentWindow.on('keyboardHeightChange', (data: number) => {
        this.keyboardHeight = px2vp(data);
      })
    })
  }

  build() {
    Column() {
      // ...

      TextInput()

      if (this.keyboardHeight > 0) {
        Row() { // 表情栏
          // ...
        }
        // ...
      }
    }
  }
}

监听获取安全区域高度

通过[window]模块的[on(‘avoidAreaChange’)]方法开启当前窗口系统规避区变化的监听,获取内容可视区域大小,同时也可以监听软键盘的弹出收起。开发者可以根据软键盘弹出之后的可视区域大小去动态的调整布局中组件的高度去适配界面。

import { window } from '@kit.ArkUI';

@Entry
@Component
struct GetSafeAreaHeightDemo {
  @State screenHeight: number = 0; // 安全区域高度
  @State isKeyBoardHidden: boolean = false; // 软键盘是否隐藏

  aboutToAppear(): void {
    window.getLastWindow(getContext(this)).then(currentWindow => {
      let property = currentWindow.getWindowProperties();
      let avoidArea = currentWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_KEYBOARD);
      // 初始化显示区域高度
      this.screenHeight = 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 = px2vp(property.windowRect.height -data.area.topRect.height - data.area.bottomRect.height);
        console.info(`screen height is: ${this.screenHeight}`);
      })
    })
  }

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

软键盘避让机制

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

软键盘默认避让效果

为了确保输入框不被软键盘挡住,系统默认提供了输入框避让软键盘的能力,结合下面这个输入框列表,介绍软键盘避让的主要表现形式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 如果当前输入框不会被软键盘遮挡,则不上抬组件,如图所示点击输入框1,组件不会上抬。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 当软键盘弹出会覆盖输入框的时候,弹窗整体会上抬。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是系统默认的软键盘避让形式只能保证输入框不被遮挡,输入框下方的组件可能就会被软键盘挡住,解决这一问题,这就需要了解软键盘的避让模式了。

软键盘避让模式

当用户在输入时,为了确保输入框不会被键盘遮挡,系统提供了避让模式来解决这一问题。开发者可以通过[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下设置百分比宽高的组件会跟随Page压缩,直接设置宽高的组件会按设置的固定大小布局。设置KeyboardAvoidMode.RESIZE时,expandSafeArea([SafeAreaType.KEYBOARD],[SafeAreaEdge.BOTTOM])不生效。
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.RESIZE);
    });
  }
}

示意效果如下,通过压缩内容区域高度实现软键盘避让:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

设置组件不避让软键盘

前面介绍的避让模式,组件会为了避让软键盘而产生位移。有时希望组件不要避让软键盘,比如软键盘的避让模式为上抬模式时,想要顶部的标题栏不要上抬。这种需求该怎么实现呢?这就需要了解[安全区域]和[expandSafeArea]属性了。

通过[expandSafeArea]属性支持组件不改变布局情况下扩展其绘制区域至安全区外,当设置expandSafeArea属性type为SafeAreaType.KEYBOARD的时候,即expandSafeArea([SafeAreaType.KEYBOARD]),系统会将软键盘区域视作安全区,从而不会避让软键盘。如果您希望某些组件不避让软键盘弹出,可以给组件设置expandSafeArea属性。

组件避让软键盘的示例效果如下,软键盘弹出时页面整体上抬,自定义标题栏固定不动

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

软键盘避让常见问题

下面列举一些常见的软键盘适配问题,帮助开发者了解软键盘的适配方法。

重要信息被软键盘遮挡

例如下面这个电子邮件的示例,内容由三部分组成:标题栏、内容区域和底部操作栏。当点击输入内容的输入框,软键盘会挡住底部的操作栏,影响用户体验,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

@Entry
@Component
struct MailHomePage1 {
  build() {
    Column() {
      this.NavigationTitle()
      this.EmailContent()
      this.BottomToolbar()
    }
    // ...
  }

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

  /**
   * Bottom toolbar area.
   */
  @Builder
  BottomToolbar() {
    Row({ space: 24 }) {
      // ...
    .width('100%')
    .height(56)
    // ...
  }

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

开发者可以通过设置软键盘的避让模式为KeyboardAvoidMode.RESIZE(压缩模式),来解决底部操作栏被遮挡的问题,设置该属性后,软键盘的避让会通过压缩内容区域的高度来实现。示例代码如下:

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.RESIZE);
    });
  }
}

需要注意的是内容区域高度的设置需要用百分比的方式实现,效果图如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过监听软键盘弹出,实现软键盘避让

上面这个示例开发者还可以通过window模块的[getWindowAvoidArea]方法,监听获取软键盘弹出,获取安全显示区域高度动态设置页面高度。示例代码如下:

import { window } from '@kit.ArkUI';

@Entry
@Component
struct MailHomePage2 {
  @State message: string = 'Hello World';
  @State screenHeight: number = 0;
  @State isKeyBoardHidden: boolean = false;

  aboutToAppear(): void {
    window.getLastWindow(getContext(this)).then(currentWindow => {
      let property = currentWindow.getWindowProperties();
      let avoidArea = currentWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
      // 初始化显示区域高度
      this.screenHeight = px2vp(property.windowRect.height - avoidArea.topRect.height - avoidArea.bottomRect.height);
      // 监视软键盘的弹出和收起
      currentWindow.on('avoidAreaChange', async data => {
        if (data.type !== window.AvoidAreaType.TYPE_KEYBOARD) {
          return;
        }
        if (data.area.bottomRect.height <= 0) {
          this.isKeyBoardHidden = true;
        } else {
          this.isKeyBoardHidden = false;
        }
        this.screenHeight = px2vp(property.windowRect.height - avoidArea.topRect.height - data.area.bottomRect.height);
      })
    })
  }

  build() {
    Column() {
      this.NavigationTitle()
      this.EmailContent()
      this.BottomToolbar()
    }
    .width('100%')
    .height(this.screenHeight) // 动态设置可视区域高度
    .expandSafeArea([SafeAreaType.KEYBOARD])
    .backgroundColor('#F1F3F5')
  }

  // ...
}

当系统的避让机制可能满足不了开发者的一些需求的时候,开发者可以尝试监听软键盘弹出,根据获取的安全区域或者软键盘高度,来调整布局大小和位置来避让软键盘。

软键盘弹出导致布局错位

内容向上滚动避让,顶部固定

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

@Entry
@Component
struct ContactPage {
  build() {
    Column() {
      Row() { // 顶部自定义标题栏
        // ...
      }
      .width('100%')
      .height('12%')

      List() { // 聊天消息区域
        // ...
      }
      .width('100%')
      .height('76%')

      Column() { // 底部消息输入框
        // ...
      }
      .width('100%')
      .height('12%')
    }
    .width('100%')
    .height('100%')
  }
}

但是由于软键盘避让默认是上抬模式,会把整个页面向上抬起,所以标题也会被顶上去,如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

@Entry
@Component
struct ContactPage {
  build() {
    Column() {
      Row() { // 顶部自定义标题栏
        // ...
      }
      .width('100%')
      .height('12%')
      .expandSafeArea([SafeAreaType.KEYBOARD])
      .zIndex(1)

      List() { // 聊天消息区域
        // ...
      }
      .width('100%')
      .height('76%')

      Column() { // 底部消息输入框
        // ...
      }
      .width('100%')
      .height('12%')
    }
    .width('100%')
    .height('100%')
  }
}

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

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

在软键盘系统避让机制中介绍过,弹窗为避让软键盘会进行避让,整体向上抬,这样可能会影响用户体验。比如下面这个评论里列表的弹窗,使用@CustomDialog实现的,示例代码如下:

@CustomDialog
struct CommentDialog {
  listData: string[] = ['评论1', '评论2', '评论3', '评论4', '评论5', '评论6', '评论7', '评论8'];
  controller?: CustomDialogController;

  build() {
    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%')
    }
    .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 { 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.RESIZE);
    });
  }
}

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

软键盘弹出时,防止Toast上抬

软键盘默认弹出的时候会将**promptAction.showToast**的Toast向上抬起,有时候可能会造成不好的体验。例如下面这个登录的示例,当Toast显示的时候,弹出软键盘会导致Toast上抬,挡住关键信息登录按钮,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以通过设置showMode为ToastShowMode.TOP_MOST来实现。showMode用于设置弹窗是否显示在应用之上,默认显示在应用内,设置为ToastShowMode.TOP_MOST之后,Toast可以显示在应用之上。

Button('登录')
  // ...
  .onClick(() => {
    promptAction.showToast({
      message: '账号或密码不能为空',
      showMode: promptAction.ToastShowMode.DEFAULT
    });
  })

效果图如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值