本頁由機器翻譯,可能有錯誤。 閱讀英文原文

元素

鍵盤

env(keyboard-inset-height) 是軟體鍵盤在畫面表面上方的高度,鍵盤隱藏時為 0。將它用作 view 的底部 padding 或 margin,可讓內容保持在鍵盤上方:當鍵盤顯示或隱藏時,view 會依鍵盤的高度騰出空間,且這項變化會以原生方式與鍵盤同步動畫。它適用於捲動檢視之外的內容,例如聊天畫面底部的訊息列;位於捲動檢視或 List 內的輸入欄位,則會以原生方式捲動至可見範圍。與它搭配的還有用來隱藏鍵盤的 Keyboard.dismiss(),以及鍵盤高度本身,可用於您自訂的版面配置。

el-keyboard.tsx
el-keyboard.tsxLive
import { mount, Scroll, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { Button, Switch } from "nsh/base";
import { Keyboard, keyboardFrame } from "nsh";
import { button, buttonText, desktop, field, tint, ui } from "./palette";

function App() {
  const draft = signal("");
  const messages = signal(["Type below: the bar stays above the keyboard."]);
  const behavior = signal<"padding" | "height">("padding");
  const enabled = signal(true);
  const send = () => {
    if (!draft().trim()) return;
    messages.update((list) => [...list, draft().trim()]);
    draft.set("");
  };
  const chip = (on: () => boolean) => button("secondary", { on, size: "sm" });
  const chipText = (on: () => boolean) => buttonText("secondary", { on });
  const isPadding = () => behavior() === "padding";
  const isHeight = () => behavior() === "height";
  return (
    <View
      style={{
        flex: 1,
        backgroundColor: behavior() === "height" ? tint(ui().info, 0.12) : ui().page,
        // Bottom padding (or margin) by the keyboard's height, while enabled.
        ...(!enabled() ? {} : behavior() === "padding" ? { paddingBottom: "env(keyboard-inset-height)" } : { marginBottom: "env(keyboard-inset-height)" }),
      }}
    >
      <View style={{ padding: 20, paddingTop: 24, paddingBottom: 8, gap: 8 }}>
        <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>keyboard inset</Text>
        <Text style={{ color: ui().muted }}>
          {desktop
            ? "Desktops type with a hardware keyboard: its height stays 0 and the view keeps its layout. Open this on a phone to see it move."
            : "Tap the message field: the bar moves up with the keyboard."}
        </Text>
        <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 6 }}>
          <Button onPress={() => behavior.set("padding")} selected={isPadding()} {...chip(isPadding)}>
            <Text style={chipText(isPadding)}>padding</Text>
          </Button>
          <Button onPress={() => behavior.set("height")} selected={isHeight()} {...chip(isHeight)}>
            <Text style={chipText(isHeight)}>height</Text>
          </Button>
          <Switch
            checked={enabled()}
            onChange={({ checked }) => enabled.set(checked)}
            class="flex-row items-center gap-2 px-1 active:opacity-70"
            trackClass="w-9 rounded-full p-0.5 transition-colors motion-reduce:transition-none"
            trackStyle={enabled() ? ui().accentFill : ui().inset}
            thumbClass="size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4"
            thumbStyle={enabled() ? { backgroundColor: ui().onAccent } : ui().raised}
          >
            <Text style={{ color: ui().text, fontSize: 13, fontWeight: "600" }}>enabled {String(enabled())}</Text>
          </Switch>
        </View>
        <Text style={{ color: ui().accent, fontSize: 13 }}>
          keyboardFrame(): height {Math.round(keyboardFrame().height)} · duration {Math.round(keyboardFrame().duration)} ms
        </Text>
      </View>

      <Scroll style={{ flex: 1 }} keyboardDismissMode="interactive">
        <View style={{ padding: 20, paddingTop: 4, gap: 8 }}>
          <Each of={messages()}>
            {(text) => (
              <View style={{ ...ui().row, alignSelf: "flex-start", maxWidth: 320, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8 }}>
                <Text style={{ color: ui().text }}>{text()}</Text>
              </View>
            )}
          </Each>
        </View>
      </Scroll>

      <View style={{ flexDirection: "row", gap: 8, padding: 12, paddingHorizontal: desktop ? 20 : 12, borderTopWidth: 1, borderTopColor: ui().border, backgroundColor: ui().page }}>
        <Input
          value={draft()}
          onChange={({ value }) => draft.set(value)}
          onSubmit={send}
          testID="message"
          returnKeyType="send"
          placeholder="Message"
          {...field({ style: { flex: 1 } })}
        />
        <Button onPress={send} {...button("primary")}>
          <Text style={buttonText("primary")}>Send</Text>
        </Button>
        <Button onPress={() => Keyboard.dismiss()} {...button()}>
          <Text style={buttonText()}>Done</Text>
        </Button>
      </View>
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

fn chat() -> impl IntoElement {
    let draft = signal(String::new());
    view()
        .flex_1()
        .bg(white())
        .pb_keyboard()
        .child(scroll().flex_1().child(text("Messages…").p_4()))
        .child(
            view()
                .flex_row()
                .gap_2()
                .p_2()
                .border_t_1()
                .border_color(rgb(0xe5e5e5))
                .child(
                    input()
                        .value(draft)
                        .on_change(move |v: &str| draft.set(v.to_owned()))
                        .placeholder("Message")
                        .flex_1()
                        .p_2(),
                ),
        )
}

完整的鍵盤高度適用於延伸至螢幕底部的 view,例如整個畫面的內容。當 view 下方還有其他元素(例如分頁列)時,請以長度值將其扣除:calc(env(keyboard-inset-height) + -49px) 會讓 view 保持在 49 pt 分頁列的上方,而 calc(env(keyboard-inset-height) + 12px) 則會在鍵盤上方加入間距。padding 最小為 0。

#Padding 或 margin

樣式騰出空間的方式
paddingBottom: "env(keyboard-inset-height)"(Tailwind pb-[env(keyboard-inset-height)],Rust .pb_keyboard())上方的內容會縮小;背景仍會延伸至螢幕底部。
marginBottom: "env(keyboard-inset-height)"(Tailwind mb-[env(keyboard-inset-height)],Rust .mb_keyboard())整個 view(包含其背景)會在鍵盤頂端結束。

鍵盤的高度只會加到同一個宣告中的長度上,因此 view 若要將它與自身的間距結合,請在 calc() 中處理。在 Rust 中,可使用 .pb_keyboard_plus(px(..)) 與 .mb_keyboard_plus(px(..)) 來加上長度。

use nsh_core::prelude::*;

fn sign_in() -> impl IntoElement {
    view()
        .mb_keyboard_plus(px(-49.0))
        .flex_1()
        .p_4()
        .gap_3()
        .child(input().placeholder("E-mail").keyboard_type(KeyboardType::Email))
}

在此範例中,view 下方的 49 pt 分頁列已經遮住了一部分的鍵盤高度。若要關閉騰出空間的行為,請不要在樣式中使用該變數,例如依據 signal 設定條件。

#隱藏鍵盤

來自 nsh 的 Keyboard.dismiss() 會讓目前取得焦點的輸入欄位失去焦點,進而隱藏鍵盤。若沒有任何輸入欄位取得焦點,則不會有任何變動。Rust 元件則呼叫 dismiss_keyboard()。

use nsh_base::button;
use nsh_core::prelude::*;

fn done() -> impl IntoElement {
    button()
        .on_press(|| dismiss_keyboard())
        .rounded_lg()
        .bg(rgb(0x2563eb))
        .px_4()
        .py_3()
        .child(text("Done").text_color(white()))
}

#鍵盤的高度

TSXRust傳回值
keyboardFrame()keyboard_height()、keyboard_duration()軟體鍵盤在 NSH 畫面表面上方的高度(以點為單位,隱藏時為 0),以及其顯示或隱藏動畫的持續時間(以毫秒為單位)。

兩者皆為響應式:當鍵盤顯示、隱藏或高度改變時,讀取它們的 effect、JSX 運算式或樣式 closure 都會重新執行。

use nsh_core::prelude::*;

fn message_bar() -> impl IntoElement {
    view()
        .flex_row()
        .p_2()
        .pb(move || {
            // The home indicator's inset only matters while the keyboard is hidden.
            let home = if keyboard_height() > Points::ZERO {
                Points::ZERO
            } else {
                safe_area().bottom
            };
            px(8.0) + home
        })
        .child(input().placeholder("Message").flex_1())
}

此高度是指鍵盤與 NSH 畫面表面重疊的部分。當視窗本身因鍵盤而縮小(例如會調整大小的 Android activity),或 NSH 嵌入在終止於鍵盤上方的 view 中時,高度為 0。

#平台說明

  • iOS:騰出的空間會以鍵盤本身的持續時間與 ease-in-ease-out 曲線進行動畫。
  • Android:當視窗因鍵盤而縮小(會調整大小的 activity)時,內容本來就會在鍵盤上方結束,此變數為 0。當鍵盤覆蓋在 view 上方時,其騰出空間的方式與 iOS 相同。
  • 桌面平台與 Mac Catalyst 使用實體鍵盤輸入:此變數為 0,view 會維持原有的版面配置。
  • Web:此變數為瀏覽器本身提供的變數(即 VirtualKeyboard API 的變數)。

關於焦點順序、return 鍵與輸入附屬工具列,請參閱表單與鍵盤;關於其他環境變數,請參閱安全區域。