Skyroc Native UI

NumberKeyboard

自定义数字键盘,用于密码、金额等场景

数字键盘(NumberKeyboard)在页面底部弹出一块自绘键盘,替代系统键盘用于密码、金额、验证码这类只需数字的输入。面板挂在 Portal 上(zIndex 900,低于 Toast 与 Notify),进出场用 Reanimated 的布局动画从自身高度之下滑入滑出。

import { NumberKeyboard } from '@skyroc/native-ui';

面板通过 Portal 渲染,请确保 App 内已经挂了 PortalHost

基础用法

visible 控制显隐,value + onChange 组成受控输入。键盘只负责算出完整的新值并抛出来,增删逻辑不用你自己拼。

NumberKeyboardBasic.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardBasic = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      {/* 键盘常驻,只切 visible:它自己会 Portal 到应用根节点,放在这里不影响定位 */}
      <NumberKeyboard
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardBasic };

何时使用

  • 支付密码、短信验证码、金额输入等只需数字、且不希望弹出系统键盘的场景。
  • 配合 PasswordInput 使用时,把 PasswordInput 的输入交给这块键盘驱动。
  • 普通文本输入请用 Input,不要用它替代系统键盘。

标题与关闭按钮

title 显示在标题栏中间,标题栏左右两侧是等宽区域,所以标题始终绝对居中。closeButtonTextdefault 主题下才会渲染标题栏右侧的关闭按钮(不传就没有)。

NumberKeyboardTitle.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardTitle = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      <NumberKeyboard
        closeButtonText="完成"
        title="请输入密码"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardTitle };

额外按键

extraKey 传字符串时是一个额外键,default 主题把它放在左下角(0 的左侧)。

NumberKeyboardExtraKey.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardExtraKey = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      <NumberKeyboard
        extraKey="."
        title="金额"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardExtraKey };

不传额外键时该位置留一个占位格,网格仍然是完整的 4×3,不会塌陷。

custom 主题

theme="custom" 在右侧加一列竖排功能区:删除键占 1 份高度、完成键占 3 份,按键网格让出 1/4 宽度。此时关闭按钮在侧边栏(文案取 closeButtonText,缺省为「完成」),标题栏不再出现关闭按钮。

NumberKeyboardCustomTheme.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardCustomTheme = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      <NumberKeyboard
        closeButtonText="完成"
        extraKey="."
        theme="custom"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardCustomTheme };

两个额外按键

extraKey 传长度为 2 的数组时是两个额外键,分别落在 0 的两侧 —— 这只在 custom 主题下成立,default 主题只取数组的第一项。

NumberKeyboardTwoExtraKeys.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardTwoExtraKeys = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      <NumberKeyboard
        extraKey={['00', '.']}
        theme="custom"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardTwoExtraKeys };

随机键序

randomKeyOrder 让 1–9 每次打开时重新洗牌(Fisher-Yates),常用于密码输入防窥屏。关闭时不重排:退场动画还在播,此刻换顺序会让数字当场跳变。

NumberKeyboardRandomOrder.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardRandomOrder = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      <NumberKeyboard
        randomKeyOrder
        title="安全键盘"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardRandomOrder };

限制长度

maxLength 达到后继续按数字键不再有反应(onInput 也不触发),删除键不受影响。

NumberKeyboardMaxLength.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardMaxLength = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      <NumberKeyboard
        maxLength={6}
        title="最多 6 位"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardMaxLength };

删除键

deleteButtonText 替换默认的退格符号 showDeleteKey={false} 隐藏删除键,位置同样留占位格保持网格对齐(custom 主题下则是侧边栏少一个键)。

NumberKeyboardHideDelete.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardHideDelete = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');
  const [showDeleteKey, setShowDeleteKey] = useState(true);

  function openKeyboard(showDelete: boolean) {
    setValue('');
    setShowDeleteKey(showDelete);
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <View className="flex-row flex-wrap gap-2">
        <Button
          variant="outline"
          onPress={() => openKeyboard(true)}
        >
          文字删除键
        </Button>
        <Button
          variant="outline"
          onPress={() => openKeyboard(false)}
        >
          隐藏删除键
        </Button>
      </View>

      <NumberKeyboard
        deleteButtonText="删除"
        showDeleteKey={showDeleteKey}
        title={showDeleteKey ? '文字删除键' : '无删除键'}
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardHideDelete };

自定义删除内容

renderDelete 返回任意节点作为删除键内容,优先级高于 deleteButtonText。按下后仍然走组件的单字符删除逻辑。

NumberKeyboardCustomDelete.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardCustomDelete = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      <NumberKeyboard
        renderDelete={() => <Text className="text-base font-medium text-primary">退一位</Text>}
        title="自定义删除键"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardCustomDelete };

事件回调

回调时机
onInput按下数字键或额外键(受 maxLength 限制)
onDelete按下删除键
onChange值变化,参数是算好的完整新值
onClose点击关闭 / 完成按钮
onBlur点击键盘外部,或点击关闭按钮(在 onClose 之后)
NumberKeyboardEvents.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardEvents = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');
  const [lastEvent, setLastEvent] = useState('尚未触发');

  function openKeyboard() {
    setVisible(true);
    setLastEvent('打开键盘');
  }

  function handleBlur() {
    setVisible(false);
    setLastEvent(current => (current === 'onClose' ? 'onClose → onBlur' : 'onBlur'));
  }

  function handleClose() {
    setVisible(false);
    setLastEvent('onClose');
  }

  function handleDelete() {
    setLastEvent('onDelete');
  }

  function handleInput(key: string) {
    setLastEvent(`onInput:${key}`);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Text color="muted">最近事件:{lastEvent}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开并观察事件
      </Button>

      <NumberKeyboard
        closeButtonText="完成"
        title="事件回调"
        value={value}
        visible={visible}
        onBlur={handleBlur}
        onChange={setValue}
        onClose={handleClose}
        onDelete={handleDelete}
        onInput={handleInput}
      />
    </View>
  );
};

export { NumberKeyboardEvents };

组件不会自己收起键盘 —— visible 始终由你控制,通常在 onBlur 里置为 false

外部保持可点

hideOnClickOutside={false} 时不铺透明遮罩,键盘不做模态遮挡,下层内容照常可点,也就不再触发 onBlur

NumberKeyboardNonModal.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardNonModal = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');
  const [count, setCount] = useState(0);

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Text color="muted">下层按钮点击次数:{count}</Text>
      <View className="flex-row flex-wrap gap-2">
        <Button
          variant="outline"
          onPress={openKeyboard}
        >
          打开键盘
        </Button>
        <Button
          variant="outline"
          onPress={() => setCount(current => current + 1)}
        >
          测试下层点击
        </Button>
      </View>

      {/* hideOnClickOutside={false} 时键盘不做模态遮挡,页面上的按钮仍可点击 */}
      <NumberKeyboard
        hideOnClickOutside={false}
        closeButtonText="收起"
        title="非模态"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardNonModal };

遮罩随 visible 立即消失而不等退场动画播完,这样收起途中的点击会落到下层内容上 —— 那正是此刻用户想点的东西。

非受控用法

不传 value 时输入值由键盘自己持有,onChange 照常抛出完整新值。注意受控传空串与不传 value 是两种语义,组件按 value !== undefined 判断。

NumberKeyboardUncontrolled.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardUncontrolled = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');
  const [session, setSession] = useState(0);

  function openKeyboard() {
    setValue('');
    setSession(current => current + 1);
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      {/* 刻意不传 value:键盘自己记输入值,这里只负责把 onChange 的结果显示出来 */}
      <NumberKeyboard
        key={session}
        maxLength={4}
        title="非受控"
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardUncontrolled };

底部安全区

safeAreaInsetBottom 默认开启,用 pb-safe-or-1 避让设备底部安全区(不依赖 useSafeAreaInsets,缺少 SafeAreaProvider 时也不会整个挂掉;安全区尺寸由应用根节点的 Uniwind.updateInsets 同步)。

NumberKeyboardSafeArea.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardSafeArea = () => {
  const [visible, setVisible] = useState(false);
  const [safeAreaInsetBottom, setSafeAreaInsetBottom] = useState(true);

  function openKeyboard(withSafeArea: boolean) {
    setSafeAreaInsetBottom(withSafeArea);
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="gap-3 bg-background p-4">
      <Text color="muted">当前:safeAreaInsetBottom={String(safeAreaInsetBottom)}</Text>
      <View className="flex-row flex-wrap gap-2">
        <Button
          variant="outline"
          onPress={() => openKeyboard(true)}
        >
          适配底部安全区
        </Button>
        <Button
          variant="outline"
          onPress={() => openKeyboard(false)}
        >
          关闭安全区适配
        </Button>
      </View>

      <NumberKeyboard
        closeButtonText="完成"
        safeAreaInsetBottom={safeAreaInsetBottom}
        title="底部安全区"
        visible={visible}
        onBlur={closeKeyboard}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardSafeArea };

样式覆盖

className 追加到面板根节点上,classNames 按 slot 细粒度覆盖,style 提供原生样式兜底:

slot作用位置
root面板根节点(底部圆角、背景、内边距)
header标题栏
headerSide标题栏左右等宽区域
title标题文字
closeBtn标题栏关闭按钮文字
body按键区与侧边栏的横向容器
keys按键网格容器
keyWrapper单个按键的外包层(只负责占格与沟槽)
key单个按键本体(圆角、背景、按下态)
keyText数字键文字
sidebarcustom 主题的右侧功能区
deleteKey侧边栏删除键
confirmKey侧边栏完成键
functionKeyText删除 / 完成等功能键的文字
NumberKeyboardStyles.tsx
Loading…
import { Button, NumberKeyboard, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const NumberKeyboardStyles = () => {
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  function openKeyboard() {
    setValue('');
    setVisible(true);
  }

  function closeKeyboard() {
    setVisible(false);
  }

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Text color="muted">当前输入:{value || '—'}</Text>
      <Button
        variant="outline"
        onPress={openKeyboard}
      >
        打开
      </Button>

      <NumberKeyboard
        className="border-t border-primary/30"
        classNames={{
          confirmKey: 'bg-success',
          keyText: 'text-3xl font-semibold',
          root: 'bg-primary/10'
        }}
        closeButtonText="完成"
        theme="custom"
        value={value}
        visible={visible}
        onBlur={closeKeyboard}
        onChange={setValue}
        onClose={closeKeyboard}
      />
    </View>
  );
};

export { NumberKeyboardStyles };

间距体系是三处联动的:每个按键外包一层 keyWrapperp-1),相邻按键之间自然形成 8px 沟槽,rootsidebar 的内边距同样取 1 —— 改刻度请三处一起改,否则键盘边缘会忽宽忽窄。

API

NumberKeyboard

属性说明类型默认值
visible是否显示键盘booleanfalse
value当前输入值;传了即为受控,长度限制与增删都以它为准string-
onChange值变化回调,参数为算好的完整新值(value: string) => void-
onInput按下数字键或额外键时触发(key: string) => void-
onDelete按下删除键时触发() => void-
onClose点击关闭 / 完成按钮时触发() => void-
onBlur点击键盘外部或关闭按钮时触发() => void-
maxLength最大输入长度numberInfinity
theme键盘主题,custom 带右侧竖排功能区'default' | 'custom''default'
title键盘标题string-
closeButtonText关闭按钮文字;default 主题传了才显示标题栏关闭按钮,custom 主题缺省为「完成」string-
extraKey额外按键,字符串为一个,长度为 2 的数组为两个(仅 custom 主题支持两个)string | [string, string]''
showDeleteKey是否显示删除键booleantrue
deleteButtonText删除键文字,不传则显示退格符号 ⌫string-
renderDelete自定义删除键内容,优先级高于 deleteButtonText() => ReactNode-
randomKeyOrder是否随机排列数字键,每次打开重新洗牌booleanfalse
hideOnClickOutside点击外部是否触发 onBlur;为 false 时不做模态遮挡,下层内容保持可点booleantrue
safeAreaInsetBottom是否适配底部安全区域booleantrue
className面板根节点类名,合并在 classNames.root 之后string-
classNames各 slot 的类名覆盖,见「样式覆盖」一节SlotClassNames<NumberKeyboardSlots>-
style面板根节点的原生样式覆盖StyleProp<ViewStyle>-

类型

import type { KeyType, NumberKeyboardProps, NumberKeyboardSlots, NumberKeyboardTheme } from '@skyroc/native-ui';

NumberKeyboardTheme

键盘主题,custom 带右侧竖排功能区(删除 + 完成)。

'custom' | 'default'

KeyType

按键类型;placeholder 只占格不渲染按钮,用于把网格撑成完整的 4×3。

'delete' | 'extra' | 'normal' | 'placeholder'

NumberKeyboardSlots

可通过 classNames 覆盖的 slot 名称。

'body' | 'closeBtn' | 'confirmKey' | 'deleteKey' | 'functionKeyText' | 'header' | 'headerSide' | 'key' | 'keys' | 'keyText' | 'keyWrapper' | 'root' | 'sidebar' | 'title'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 NumberKeyboardSlots。

Partial<Record<Slots, string>>

包内还导出了 numberKeyboardVariants