基于原生能力的键盘控制

基于原生能力的键盘控制


前言

应用通常使用键盘的方式,系统键盘的弹出收起,获焦失焦,高度监听,安全避让等。

应用经常会遇到如下的业务诉求:

场景一:进入页面TextInput获焦,弹出系统键盘。

场景二:点击按钮或其他事件触发TextInput获焦,弹出系统键盘。

场景三:键盘弹出后只上抬特定的输入组件。

场景四:监听键盘高度,在键盘弹出后让组件上移,键盘收起后让组件恢复。

场景五:设置窗口在键盘抬起时的页面避让模式为上抬,压缩。

一、进入页面TextInput获焦

1、方案

通过defaultFocus通用属性设置,实现第一次进入页面后弹出键盘

2、核心代码

bash 复制代码
TextInput({ text: $$this.username, placeholder: '请输入用户名' })
  .placeholderColor('#D4D3D1')
  .backgroundColor(this.isUserNameFocus ? '#80FFFFFF' : '#ffffff')
  .width(260)
  .borderRadius(8)
  .id('username')
  .margin({ top: 10, bottom: 10 })
  .onFocus(() => {
    this.isUserNameFocus = true
  })
  .onBlur(() => {
    this.isUserNameFocus = false
  })
  .defaultFocus(true)   // 进入页面默认获焦

二、点击按钮或其他事件触发TextInput获焦

1、方案

通过focusControl.requestFocus,实现输入账号后,点击登录按钮后,代码主动设置TextInput获取焦点

2、核心代码

bash 复制代码
Button('登  录')
  .width(200)
  .height(45)
  .fontSize(28)
  .type(ButtonType.Normal)
  .backgroundColor('#30FFFFFF')
  .border({ width: 1, color: Color.White, radius: 8 })
  .margin({ top: 50, bottom: 60 })
  .onClick(() => {
    let LoginForm: LoginForm = {
      username: this.username,
      password: this.password
    }
    let requestId = ''
    if (!LoginForm.username) {
      requestId = 'username'
    } else if (!LoginForm.password) {
      requestId = 'password'
    } else {
      promptAction.showToast({ message: 'Login success' })
      return
    }
    let res = focusControl.requestFocus(requestId) // 使选中的this.selectId的组件获焦,这里要注意获焦的id要与组件的id保持一致
    promptAction.showToast({ message: requestId + '不能为空' })
  })

三、键盘弹出后只上抬特定的输入组件

1、方案

通过expandSafeArea通用属性设置,实现只上抬红框圈住的特定组件

2、核心代码

bash 复制代码
Column(){
    // 在页面跟容器设置安全区域这样页面就不会上抬了只有对应的输入框会进行上抬
}
.expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

四、监听键盘高度

1、方案

先通过expandSafeArea禁止自动键盘弹出上移页面。然后通过window.on('avoidAreaChange')和windowClass.on('keyboardHeightChange'),实现监听键盘高度,上抬整个页面view,让输入框(生命周期价值输入框)组件能显示在键盘上方

2、核心代码

bash 复制代码
@State screenHeight: Length = 0
 
onPageShow(): 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('keyboardHeightChange', async data => {
        // 这里通过监听键盘高度变化来改变跟容器高度变化
        this.screenHeight = px2vp(property.windowRect.height - avoidArea.topRect.height - data);
    })
  })
  // ...
}
 
Column() {
   ...
}
.width('100%')
.height(this.screenHeight)
.expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

五、设置窗口在键盘抬起时的页面避让模式为上抬,压缩

1、方案

点击生命周期输入框,通过windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode,实现设置窗口在键盘抬起时的页面避让模式,这种情况下当键盘弹起的时候页面会自动压缩

2、核心代码

bash 复制代码
onPageShow(): void {
    // ...
    this.windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE)
    // 在RESIZE模式下当键盘弹起的时候页面会进行压缩
    // ...
}
 
onPageHide(): void {
    this.windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET)
    // 在OFFSET模式下当键盘弹起的时候页面会进行上抬
}

demo代码

Index.ets

bash 复制代码
import { router } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  build() {
    Row() {
      Column() {
        Button('进入页面获焦')
          .margin({ top: 10, bottom: 10 })
          .onClick(() => {
            router.pushUrl({ url: 'pages/Login', params: { pageId: '1' } })
          })
        Button('事件触发主动获焦')
          .margin({ top: 10, bottom: 10 })
          .onClick(() => {
            router.pushUrl({ url: 'pages/Login', params: { pageId: '2' } })
          })
        Button('键盘弹出不上移组件')
          .margin({ top: 10, bottom: 10 })
          .onClick(() => {
            router.pushUrl({ url: 'pages/Register', params: { pageId: '3' } })
          })
        Button('监听键盘高度变化')
          .margin({ top: 10, bottom: 10 })
          .onClick(() => {
            router.pushUrl({ url: 'pages/Register', params: { pageId: '4' } })
          })
        Button('设置页面避让模式')
          .margin({ top: 10, bottom: 10 })
          .onClick(() => {
            router.pushUrl({ url: 'pages/Register', params: { pageId: '5' } })
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

Login.ets

bash 复制代码
import { promptAction, router } from '@kit.ArkUI'

interface LoginForm {
  username: string
  password: string
}

export interface routerParam {
  pageId: string
}

@Entry
@Component
struct Login {
  @State username: string = ''
  @Watch('passwordChange') @State password: string = ''
  @State isUserNameFocus: boolean = false
  @State isPasswordFocus: boolean = false
  @State rememberPassword: boolean = false
  @State showPasswordIcon: boolean = false
  private controller: TextInputController = new TextInputController()
  @State routerParams: routerParam = { pageId: '0' }

  passwordChange() {
    this.showPasswordIcon = !!this.password
  }

  onPageShow(): void {
    this.routerParams = router.getParams() as routerParam
    console.log(this.routerParams.pageId)
  }

  build() {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
      Row() {
        Image($r('app.media.avatar')).width(60).height(60)
      }
      .borderRadius(50)
      .width(100)
      .height(100)
      .justifyContent(FlexAlign.Center)
      .backgroundColor('#FFFFFF')
      .margin({ bottom: 30 })


      TextInput({ text: $$this.username, placeholder: '请输入用户名', controller: this.controller })
        .placeholderColor('#D4D3D1')
        .backgroundColor(this.isUserNameFocus ? '#80FFFFFF' : '#ffffff')
        .width(260)
        .borderRadius(8)
        .id('username')
        .margin({ top: 10, bottom: 10 })
        .onFocus(() => {
          this.isUserNameFocus = true
        })
        .onBlur(() => {
          this.isUserNameFocus = false
        })
        .defaultFocus(this.routerParams.pageId === '1') // 进入页面默认获焦

      TextInput({ text: $$this.password, placeholder: '请输入密码', controller: this.controller })
        .placeholderColor('#D4D3D1')
        .type(InputType.Password)
        .showPasswordIcon(this.showPasswordIcon)
        .backgroundColor(this.isPasswordFocus ? '#80FFFFFF' : '#ffffff')
        .width(260)
        .borderRadius(8)
        .id('password')
        .margin({ top: 10, bottom: 10 })
        .onFocus(() => {
          this.isPasswordFocus = true
        })
        .onBlur(() => {
          this.isPasswordFocus = false
        })
      Row() {
        Checkbox()
          .select($$this.rememberPassword)
          .unselectedColor('#ffffff')
          .width(16)
          .borderRadius(8)
          .backgroundColor(Color.White)
        Text('记住密码')
          .fontColor('#ffffff')
          .onClick(() => {
            this.rememberPassword = !this.rememberPassword
          })
      }.width(260)

      Button('登  录')
        .width(200)
        .height(45)
        .fontSize(28)
        .type(ButtonType.Normal)
        .backgroundColor('#30FFFFFF')
        .border({ width: 1, color: Color.White, radius: 8 })
        .margin({ top: 50, bottom: 60 })
        .onClick(() => {
          let LoginForm: LoginForm = {
            username: this.username,
            password: this.password
          }
          let requestId = ''
          // todo: 无法使用for..in遍历对象
          if (!LoginForm.username) {
            requestId = 'username'
          } else if (!LoginForm.password) {
            requestId = 'password'
          } else {
            promptAction.showToast({ message: 'Login success' })
            return
          }
          focusControl.requestFocus(requestId) // 使选中的this.selectId的组件获焦
          promptAction.showToast({ message: requestId + '不能为空' })
        })

      Row() {
        Text('忘记密码')
          .fontColor(Color.White)
        Text('注册')
          .fontColor(Color.White)
          .onClick(() => {
            router.pushUrl({ url: 'pages/Register', params: { pageId: '3' } })
          })
      }.justifyContent(FlexAlign.SpaceBetween)
      .width(260)
    }
    .width('100%')
    .height('100%')
    .expandSafeArea([SafeAreaType.SYSTEM, SafeAreaType.KEYBOARD], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    .backgroundImage($r('app.media.bg'))
    .backgroundImageSize(ImageSize.Cover)
    .onTouch(() => {
      this.controller.stopEditing()
    })
  }
}

Register.ets

bash 复制代码
import { KeyboardAvoidMode, router, window } from '@kit.ArkUI';
import { routerParam } from './Login'

@Entry
@Component
struct Register {
  @State routerParams: routerParam = { pageId: '0' }
  @State screenHeight: Length = '100%'
  private windowStage = AppStorage.get('windowStage') as window.WindowStage

  onPageShow(): void {
    this.routerParams = router.getParams() as routerParam
    if (this.routerParams.pageId == '5') {
      this.windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE)
    }

    if (this.routerParams.pageId == '4') {
      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;
          }
          this.screenHeight =
            px2vp(property.windowRect.height - avoidArea.topRect.height - data.area.bottomRect.height);
        })
      })
    }
  }

  onPageHide(): void {
    this.windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET)
  }

  build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) {
      Row() {
        Image($r('app.media.back')).width(24)
          .position({ x: 16, y: 10 })
          .onClick(() => {
            router.back()
          })
        Text('用户注册')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
      }
      .height(45)
      .borderWidth({ bottom: 1 })
      .borderColor('#EAEEF5')
      .width('100%')
      .justifyContent(FlexAlign.Center)

      Column() {
        Image($r('app.media.logo'))
          .width(240)
      }

      // 表单主体
      List({ space: 16 }) {
        ListItem() {
          FormItem({ label: '用户名', placeholder: '请输入用户名' })
        }

        ListItem() {
          FormItem({ label: '密码', placeholder: '请输入密码' })
        }

        ListItem() {
          FormItem({ label: '手机号', placeholder: '请输入手机号' })
        }

        ListItem() {
          FormItem({ label: '邮箱', placeholder: '请输入邮箱地址' })
        }

        ListItem() {
          Column() {
            Text('个人简介')
              .margin({ bottom: 16 })
            TextArea({ placeholder: '介绍点啥吧...' })
              .borderRadius(0)
              .borderWidth({ bottom: 1 })
              .borderColor('#EAEEF5')
              .height(100)
          }.alignItems(HorizontalAlign.Start)
        }
      }
      .padding(10)
      .layoutWeight(1)
      .edgeEffect(EdgeEffect.None)
      .scrollBar(BarState.Off)
      .expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

      Row() {
        Button('提交').type(ButtonType.Normal)
          .fontColor(Color.White)
          .backgroundColor('#59BEB7')
          .width('100%')
      }
      .padding(16)
      .shadow({ radius: 20, color: '#30000000' })
      .width('100%')
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
    }.width('100%').height(this.screenHeight)
    .expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }
}

@Component
struct FormItem {
  @Prop label: string
  @Prop placeholder: string
  @State value: string = ''

  build() {
    Column() {
      Text(this.label)
        .margin({ bottom: 16 })
      TextInput({ text: this.value, placeholder: this.placeholder })
        .backgroundColor(Color.Transparent)
        .borderRadius(0)
        .borderWidth({ bottom: 1 })
        .borderColor('#EAEEF5')
    }.alignItems(HorizontalAlign.Start)
    .expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }
}
相关推荐
ajassi200038 分钟前
开源 Arkts 鸿蒙应用 开发(五)控件组成和复杂控件
华为·开源·harmonyos
liuhaikang40 分钟前
我做个一个APP叫【图影工具箱】:一站式解决视频提取音频和加水印的鸿蒙神器
harmonyos
谢道韫6662 小时前
鸿蒙组件通用事件开发全攻略:从基础交互到工程实践
microsoft·交互·harmonyos
Sally璐璐6 小时前
HarmonyOS开发利器:ArkTS全解析
华为·harmonyos
移动端开发者8 小时前
鸿蒙Next显示动画animateTo介绍
harmonyos
移动端开发者8 小时前
鸿蒙Next使用AudioCapturer实现音频录制和AI语言转文字
harmonyos
移动端开发者8 小时前
鸿蒙Next选择按钮Toggle、Checkbox、Radio介绍
harmonyos