本页目录
概述环境要求一、业务场景与要点速览场景选择指引二、通用最佳实践1. useWindowHeight — 获取安全的一屏高度2. 键盘自动收起3. 聚焦单容器滚动 [iOS]4. 弹窗相对外部页面位置固定 [Android]5. 聚焦禁止滚动 [iOS]关于 @ts-ignore 的说明三、各场景要点详解1. 浏览/轻量输入框表单型 + 键盘自动收起键盘出现时,滑动页面自动收起键盘页面高度使用 useWindowHeight [Android]底部按钮位置固定 [Android]2. 多输入框表单型 + 键盘固定底部按钮固定贴底,键盘出现时安卓自动上浮 [Android]避免双层滚动容器 [iOS]保持键盘常驻3. 弹框轻量输入框表单型 + 键盘自动收起弹窗内受控 focus + onTouchMove 失焦使用 useWindowHeight 计算底部按钮位置 [Android]弹框相对外部容器位置固定 [Android]4. 弹窗多输入框表单型 + 键盘固定Popup 控制背景可滚动弹窗内底部按钮在键盘弹起时自动上浮 [Android]弹框位置相对外部页面固定 [Android]5. 弹窗聚焦弹框输入框聚焦期间页面禁止滚动6. 卡片高度固定 + 一屏固定 + 键盘固定容器高度使用 useWindowHeight,严格一屏Textarea 聚焦时禁用页面滚动 [iOS]底部按钮相对一屏容器绝对定位7. 卡片高度自适应 + 超出一屏滚动 + 键盘固定容器高度 useWindowHeight,内部 ScrollView 占满剩余空间Field textarea 使用 autosize,卡片高度随内容变化底部按钮在 ScrollView 外,保持贴底四、常见问题(FAQ)Q1: 为什么安卓设备首次获取 useableWindowHeight 为 0?Q2: lockScroll={false} 会不会导致背景页面跟着滚动?Q3: 为什么设置了 disableScroll: false 页面还是不能滚动?Q4: iOS 和 Android 在聚焦场景的主要差异是什么?Q5: 什么时候用 position: fixed,什么时候用 position: absolute?

概述

用户点击输入框时键盘会弹起,若处理不当容易出现页面被压缩、底部按钮上浮、无法滚动等问题。本文按常见业务场景(如全屏表单、弹窗内输入、多输入框等),依照涂鸦 UED 规范整理了一套做法和可运行 Demo,方便直接复用并避开常见坑。Github demo 源码

环境要求

依赖项最低版本
APP 版本7.3.0(ty.enablePageScroll 等 API 需要此版本)
@ray-js/smart-ui请使用最新版本

说明: 文中涉及的 ty.enablePageScroll 等 API 仅在 APP 7.3.0 及以上版本支持,低版本请做好兼容处理。


一、业务场景与要点速览

示意图场景核心要点关键 API / 组件
浏览/轻量输入框表单型 + 键盘自动收起全屏表单、滑动自动收键盘、底部按钮不上浮useWindowHeight, onTouchMove, ScrollView
多输入框表单型 + 键盘固定多输入框、导航栏固定、底部按钮安卓自动上浮position: fixed, disableScroll
弹框轻量输入框表单型 + 键盘自动收起弹窗 + 少量输入框、滑动自动收键盘、底部按钮不上浮Popup, useWindowHeight, onTouchMove
弹窗多输入框表单型 + 键盘固定弹窗 + 多输入框、底部按钮安卓自动上浮Popup, lockScroll={false}, position: fixed
弹窗聚焦系统级输入弹框、聚焦期间禁止滚动DialogInstance.input, ty.enablePageScroll
卡片高度固定 + 一屏固定 + 键盘固定一屏布局、固定高度 Field、聚焦禁止滚动useWindowHeight, position: absolute
卡片高度自适应 + 超出一屏滚动 + 键盘固定ScrollView 滚动、autosize textarea、按钮贴底useWindowHeight, ScrollView, autosize

场景选择指引

flowchart TD
    A["是否在弹窗内?"] -->|否| B["输入框数量?"]
    A -->|是| C["弹窗类型?"]
    B -->|"单个/少量"| D["场景1: 浏览/轻量输入框 + 键盘自动收起"]
    B -->|多个| E["场景2: 多输入框表单 + 键盘固定"]
    B -->|"卡片布局"| I["是否超出一屏?"]
    I -->|否| J["场景6: 卡片固定高度 + 一屏固定"]
    I -->|是| K["场景7: 卡片自适应 + 超出一屏滚动"]
    C -->|"系统弹框"| H["场景5: 弹窗聚焦"]
    C -->|"自定义弹窗 + 少量输入框"| F["场景3: 弹框轻量输入框 + 键盘自动收起"]
    C -->|"自定义弹窗 + 多输入框"| G["场景4: 弹窗多输入框 + 键盘固定"]

二、通用最佳实践

以下通用知识会在各场景中反复使用,建议先行了解。

1. useWindowHeight — 获取安全的一屏高度

100vh 代表整个 WebView 容器高度,但在安卓设备上键盘弹出时 WebView 会被压缩,导致底部内容上浮。为了让页面在聚焦时仍保持整屏高度,需要获取一个固定的像素值。

注意事项:

  • 通过 getSystemInfoSync().useableWindowHeight 可获取页面容器可用高度
  • 安卓设备在页面初始化时 useableWindowHeight 值为 0(逻辑层异步获取容器高度),因此需要滞后获取
  • 推荐在第一次聚焦事件时获取

建议封装为自定义 Hook:

import { useState, useCallback } from 'react';
import { getSystemInfoSync } from '@ray-js/ray';

/**
 * 获取安全的一屏高度
 *
 * 为什么不直接用 100vh?
 * - Android 键盘弹出时 WebView 容器会被压缩,100vh 随之变小
 * - useableWindowHeight 返回的是固定的物理像素值,不受键盘影响
 *
 * 为什么不在初始化时获取?
 * - Android 上 useableWindowHeight 初始值为 0(异步获取),
 *   需滞后到首次聚焦时获取
 */
function useWindowHeight(initialHeight = '100vh') {
  const [height, setHeight] = useState(initialHeight);

  const ensureHeight = useCallback(() => {
    if (height === initialHeight) {
      // useableWindowHeight 未包含在官方 TS 类型定义中,但运行时可用
      const info = getSystemInfoSync() as Record<string, any>;
      const { useableWindowHeight } = info;
      if (useableWindowHeight) {
        setHeight(`${useableWindowHeight}px`);
      }
    }
  }, [height, initialHeight]);

  return { height, ensureHeight };
}

后续各场景代码中将直接使用 useWindowHeight,不再重复获取逻辑。

2. 键盘自动收起

通过 Input 组件的受控 focus + 容器 onTouchMove 调用 setFocus(false) 即可。

<Input
  focus={focus}
  onFocus={() => setFocus(true)}
  onBlur={() => setFocus(false)}
  placeholder="请输入内容"
/>

<View onTouchMove={() => focus && setFocus(false)}>
  {/* 页面内容 */}
</View>

注意: 如果内部有阻止冒泡的行为,onTouchMove 事件可能无法触发。

3. 聚焦单容器滚动 [iOS]

iOS 聚焦时会默认在 WebView 外层包裹一层滚动容器。如果页面内部也使用了 ScrollView,会出现双层滚动容器——滚动过快时页面可能卡住。

解决方案: 使用页面级滚动,在 index.config.ts 中设置:

export default {
  // ...
  disableScroll: false,
};

此时顶部导航、底部按钮等需要用 fixed 定位,中间的内容部分上下预留 padding 滚动即可。

4. 弹窗相对外部页面位置固定 [Android]

安卓设备在弹窗场景聚焦时,可以通过让根节点使用 100vh 作为计算基准,让容器自适应压缩,保持弹窗上下位置不变。

.popupContainer {
  height: calc(100vh - 50px);
}

iOS 的聚焦位置不可控,此方案仅对 Android 有效。

5. 聚焦禁止滚动 [iOS]

iOS 在聚焦时默认可滚动,且无法控制。在 APP 7.3.0 及以上版本中,可在 onFocus 回调内调用:

// enablePageScroll 在较新版本的类型定义中可用
// 如果当前类型定义未包含,可通过模块扩展解决(见下方说明)
ty.enablePageScroll?.({ scrollEnable: false });

注意:一旦设置了不可滚动,非聚焦场景也会生效。建议在 onBlur 时恢复:

ty.enablePageScroll?.({ scrollEnable: true });

关于 @ts-ignore 的说明

文档中部分 API 调用可能出现 TypeScript 类型报错,原因如下:

API原因建议
getSystemInfoSync().useableWindowHeight该字段未包含在官方 TS 类型定义中,但运行时可用使用类型断言 as Record<string, any>
ty.enablePageScroll该 API 在 APP 7.3.0+ 才支持,类型定义可能未覆盖使用可选链 ?. 调用

推荐做法: 通过模块声明扩展类型,避免直接使用 @ts-ignore:

types/ray-js.d.ts
// types/ray-js.d.ts
declare module '@ray-js/ray' {
  interface SystemInfo {
    useableWindowHeight?: number;
  }
}

三、各场景要点详解

1. 浏览/轻量输入框表单型 + 键盘自动收起

键盘出现时,滑动页面自动收起键盘

利用 Input 组件的 focus 属性 + 容器 onTouchMove,滚动页面时输入框自动失焦收起键盘。(详见"通用最佳实践 — 键盘自动收起")

页面高度使用 useWindowHeight [Android]

使用 useWindowHeight Hook 获取准确的一屏高度,防止安卓设备键盘弹出后页面压缩。(详见"通用最佳实践 — useWindowHeight")

const { height, ensureHeight } = useWindowHeight();

const onInputFocus = () => {
  ensureHeight();
  setFocus(true);
};

底部按钮位置固定 [Android]

禁止使用 position: fixed,否则安卓键盘弹出时按钮会上浮。

可以使用根节点固定高度 + position: relative,按钮区域使用 position: absolute;或者 flex 自适应布局充满全屏。

<View className={styles.container} style={{ height }} onTouchMove={() => focus && setFocus(false)}>
  <NavBar />
  <ScrollView scrollY className={styles.content}>
    {/* 表单内容 */}
  </ScrollView>
  <View className={styles.buttonContainer}>{/* 底部按钮 */}</View>
</View>
.container {
  display: flex;
  flex-direction: column;
  .content {
    flex: 1;
  }
  .buttonContainer {
    height: 192rpx;
  }
}

2. 多输入框表单型 + 键盘固定

底部按钮固定贴底,键盘出现时安卓自动上浮 [Android]

底部操作区使用 position: fixed 定位,不随内容滚动。记得给滚动容器预留底部 padding,防止底部按钮遮挡滚动内容。

.buttonContainer {
  position: fixed;
  bottom: 0;
}

避免双层滚动容器 [iOS]

iOS 默认聚焦时会给小程序容器外层包裹一层滚动容器,如果页面内部也有滚动容器,容易出现双层滚动——滚动过快时页面卡住。建议使用页面级滚动。(详见"通用最佳实践 — 聚焦单容器滚动")

在 index.config.ts 中设置:

export default {
  // ...
  disableScroll: false,
};

如果采用自定义导航栏,则设置 position: fixed 吸顶,内容区通过 CSS 变量预留 statusBar + navbar 高度,避免被遮挡:

<View className={styles.container}>
  <NavBar className={styles.navBar} />
  <View className={styles.content}>{/* 表单内容 */}</View>
  <View className={styles.buttonContainer}>{/* 底部按钮 */}</View>
</View>
.navBar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
}
.content {
  padding-top: calc(var(--app-device-status-height, 20px) + var(--app-device-navbar-height, 46px));
  padding-bottom: 192rpx;
}

保持键盘常驻

同系统默认行为,无需额外处理。

3. 弹框轻量输入框表单型 + 键盘自动收起

弹窗内受控 focus + onTouchMove 失焦

与场景 1 的"键盘自动收起"一致。Popup 内使用 ScrollView + 受控 focus,在弹窗内容容器上监听 onTouchMove,聚焦时失焦收起键盘。

<Popup position="bottom" round show={showPopup} closeable onClose={() => setShowPopup(false)}>
  <View className={styles.popupContainer} onTouchMove={() => focus && setFocus(false)}>
    <ScrollView scrollY className={styles.popupContent}>
      <Input
        focus={focus}
        onFocus={() => setFocus(true)}
        onBlur={() => setFocus(false)}
        placeholder="请输入内容"
      />
    </ScrollView>
  </View>
</Popup>

使用 useWindowHeight 计算底部按钮位置 [Android]

安卓机上使用 useWindowHeight 获取高度,并计算按钮 top,保证相对可视区域固定。

const { height, ensureHeight } = useWindowHeight();

const onInputFocus = () => {
  ensureHeight();
  setFocus(true);
};

// 按钮容器
<View
  className={styles.buttonContainer}
  style={{
    top: `calc(${height} - 192rpx - 50px)`,
  }}
>
  {/* 底部按钮 */}
</View>;

弹框相对外部容器位置固定 [Android]

不可以设置固定的 px 或 rpx 值,会导致安卓设备键盘出现时弹框被向上顶。设置 calc(100vh - 50px) 的高度可以让容器自适应压缩。(详见"通用最佳实践 — 弹窗相对外部页面位置固定")

<View className={styles.container}>
  <NavBar />
  <Popup position="bottom">
    <View className={styles.popupContainer}>
      <ScrollView scrollY className={styles.popupContent}>
        {/* 弹窗内容 */}
      </ScrollView>
    </View>
  </Popup>
</View>
.popupContainer {
  height: calc(100vh - 50px);
}

4. 弹窗多输入框表单型 + 键盘固定

使用 lockScroll={false} 允许背景滚动。在键盘聚焦场景,用户可以通过慢速拖动使最外层容器滚动。

<Popup lockScroll={false} position="bottom">
  {/* 弹窗内容 */}
</Popup>

弹窗内底部按钮在键盘弹起时自动上浮 [Android]

安卓设备上使用 position: fixed 布局,让键盘弹出时底部按钮刚好上浮到键盘上方。记得中间滚动容器内预留底部空间,防止按钮遮挡内容。

.buttonContainer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 10;
}

弹框位置相对外部页面固定 [Android]

不可以设置固定的 px 或 rpx 值,会导致安卓设备键盘弹出时整个弹框被向上顶。设置 calc(100vh - 50px) 的高度可以让容器自适应压缩。(详见"通用最佳实践 — 弹窗相对外部页面位置固定")

<View className={styles.container}>
  <NavBar />
  <Popup position="bottom">
    <View className={styles.popupContainer}>
      <ScrollView scrollY className={styles.popupContent}>
        {/* 弹窗内容 */}
      </ScrollView>
    </View>
  </Popup>
</View>
.popupContainer {
  height: calc(100vh - 50px);
}

5. 弹窗聚焦

弹框输入框聚焦期间页面禁止滚动

方案 A:使用 SmartUI Dialog 组件(推荐)

调用 SmartUI 的 DialogInstance.input 系统级输入弹框,其内部已禁用页面滚动,无需额外处理。

常用参数示例:

DialogInstance.input({
  title: '请输入',
  placeholder: '输入内容',
  maxlength: 20,
  onConfirm: value => {
    console.log('用户输入:', value);
  },
  onCancel: () => {
    console.log('用户取消');
  },
});

方案 B:使用自定义弹框

如果使用自定义弹框,需要手动管理页面滚动状态:

// 触发弹框出现
const showDialog = () => {
  // 禁用页面滚动(APP 7.3.0+ 支持)
  ty.enablePageScroll?.({ scrollEnable: false });
  // 出现弹框逻辑...
};

// 弹框关闭回调
const closeMyDialog = () => {
  // 恢复页面滚动
  ty.enablePageScroll?.({ scrollEnable: true });
  // 关闭弹框逻辑...
};

注意: 弹框关闭后务必恢复滚动,否则页面将无法滚动。

6. 卡片高度固定 + 一屏固定 + 键盘固定

容器高度使用 useWindowHeight,严格一屏

通过 useWindowHeight 设置容器高度,避免使用 100vh 导致键盘出现后容器被压缩。

const { height, ensureHeight } = useWindowHeight();

const onTextareaFocus = () => {
  ensureHeight();
  // 禁用页面滚动(APP 7.3.0+ 支持)
  ty.enablePageScroll?.({ scrollEnable: false });
};
<View className={styles.container} style={{ height }}>
  {/* 页面内容 */}
</View>

Textarea 聚焦时禁用页面滚动 [iOS]

在 Field 的 onFocus 中调用 ty.enablePageScroll({ scrollEnable: false }),阻止 iOS 系统在聚焦时的默认可滚动行为。

<Field type="textarea" onFocus={onTextareaFocus} placeholder="请输入留言内容" />

底部按钮相对一屏容器绝对定位

按钮使用 position: absolute + bottom,相对固定高度的根容器定位,防止键盘出现时上浮。

.container {
  position: relative;
  /* 高度实际由 style={{ height }} 注入 useWindowHeight 的值 */
}
.button {
  position: absolute;
  bottom: 112rpx;
  left: 50%;
  transform: translateX(-50%);
}

7. 卡片高度自适应 + 超出一屏滚动 + 键盘固定

容器高度 useWindowHeight,内部 ScrollView 占满剩余空间

外层容器用 useWindowHeight 定高,内层 ScrollView scrollY 使用 flex: 1 占满,实现一屏内滚动。

const { height, ensureHeight } = useWindowHeight();

const onTextareaFocus = () => {
  ensureHeight();
};
<View className={styles.container} style={{ height }}>
  <NavBar />
  <ScrollView scrollY className={styles.content}>
    {/* 卡片列表 */}
  </ScrollView>
  <View className={styles.buttonContainer}>{/* 底部按钮 */}</View>
</View>
.container {
  display: flex;
  flex-direction: column;
  .content {
    flex: 1;
  }
  .buttonContainer {
    height: 192rpx;
  }
}

Field textarea 使用 autosize,卡片高度随内容变化

通过 autosize 的 maxHeight / minHeight 控制多行输入高度,避免固定高度导致内容被裁切或留白过多。

import { Field } from '@ray-js/smart-ui';

<Field
  type="textarea"
  hiddenLabel
  autosize={{
    maxHeight: '436rpx',
    minHeight: '80rpx',
  }}
  placeholder="请输入留言内容"
/>;

底部按钮在 ScrollView 外,保持贴底

按钮容器放在 ScrollView 外、根容器内,使用 flex 或固定高度,键盘弹起时仍贴底,不被顶出视野。


四、常见问题(FAQ)

Q1: 为什么安卓设备首次获取 useableWindowHeight 为 0?

安卓上逻辑层是异步获取容器高度的,页面初始化时尚未完成获取。建议在首次聚焦事件中获取,此时容器高度已就绪。

Q2: lockScroll={false} 会不会导致背景页面跟着滚动?

会。设置 lockScroll={false} 后,用户慢速拖动时可能触发外层页面滚动。如果不希望这种行为,请保持默认的 lockScroll={true},但需注意这可能影响弹窗内的键盘聚焦体验。

Q3: 为什么设置了 disableScroll: false 页面还是不能滚动?

检查以下几点:

  1. 页面内容高度是否确实超过了视口高度
  2. 是否在其他地方调用了 ty.enablePageScroll?.({ scrollEnable: false }) 但未恢复
  3. 是否有 overflow: hidden 等 CSS 样式阻止了滚动

Q4: iOS 和 Android 在聚焦场景的主要差异是什么?

行为iOSAndroid
键盘弹出时容器变化不压缩 WebView压缩 WebView(100vh 变小)
聚焦时页面滚动默认可滚动(7.3.0+ 可禁用)默认不可滚动
position: fixed 按钮位置不变随键盘上浮
外层滚动容器系统自动包裹无

Q5: 什么时候用 position: fixed,什么时候用 position: absolute?

  • fixed:用于期望按钮随键盘上浮的场景(如场景 2、4),按钮相对于视口定位
  • absolute:用于期望按钮位置不变的场景(如场景 1、6),按钮相对于固定高度的父容器定位