文档

平台特定代码

同一个 NSH 包可以在 iOS、Android、macOS、Windows 和 Linux 上运行(也可以在 Mac Catalyst 中运行),还可以通过 Web 运行时在浏览器中运行。应用的大部分代码根本不需要判断平台:元素、样式、布局和模块在各处的行为都相同,你用它们绘制的组件在各处看起来也都一样。当某样东西确实只属于一个平台时,NSH 给它安排了明确的位置,让平台代码一目了然、范围可控:

RustTSX用途
构建方法(.ripple(..)、.input_accessory(..))ios={{…}}、android={{…}}、desktop={{…}}、web={{…}}只有一个平台才有的属性。
on_back_press、nsh_core::element::desktopnsh/mobile、nsh/desktop只有移动平台或只有桌面才有的 API。
os()、idiom()、is_desktop()platform.os、platform.select按平台选择值或代码。
.when(os() == Os::Ios, ..)ios: / android: / web: 类按平台设置样式。

#平台专属属性

在 Rust 中,只有一个平台才有的属性和其他属性一样是构建方法(Android 的 .ripple(..)、.elevation(..),iOS 的 .input_accessory(..),桌面的 .window_drag(..));其他平台会忽略它。在 TSX 中,只存在于某一个平台的属性,归入以该平台命名的对象中。每个元素都接受 ios、android、desktop(macOS、Windows、Linux,以及界面风格为桌面的浏览器)和 web(浏览器);在其他平台上这个对象会被忽略,在它自己的平台上只应用其中的键。桌面浏览器同时应用 desktop 和 web。

use nsh_base::dialog;
use nsh_core::{
    Ripple,
    element::{AutoFill, Orientation}, prelude::*,
};

view()
    .on_press(open)
    .active(|s| s.opacity(0.6))
    .ripple(Ripple {
        color: Some(rgba(0x0000001f).into()),
        ..Ripple::default()
    })
    .elevation(px(4.0))
    .p_4()
    .rounded(px(12.0))
    .bg(white())
    .child(text("Open"))

input().placeholder("Code").auto_fill(AutoFill::OneTimeCode)

when(shown, || {
    dialog()
        .supported_orientations([Orientation::Portrait])
        .status_bar_translucent(true)
        .child(…)
})

TSX 的这些对象和其他属性一样是响应式的;从对象中移除的键会被清除。

目前已有的:

RustTSX适用于说明
.ripple(Ripple { .. })android={{ ripple }}所有元素按下时的 Material 水波纹:颜色、无边界、半径、前景。
.elevation(..)android={{ elevation }}所有元素根据视图形状生成的 Material 阴影,单位为点。
android={{ accessibilityLiveRegion }}所有元素"none"、"polite" 或 "assertive":TalkBack 播报视图的变化。
.status_bar_translucent(..)android={{ statusBarTranslucent }}Dialog对话框的内容延伸到状态栏下方。
.input_accessory(..)ios={{ inputAccessoryViewID }}Input显示在键盘上方的 InputAccessoryView。
.auto_fill(..)ios={{ textContentType }}InputiOS 对自动填充类型的叫法(Rust 的 AutoFill 覆盖两个平台)。
.supported_orientations(..)ios={{ supportedOrientations }}Dialog允许的方向。
.back_forward_gestures(..)ios={{ allowsBackForwardNavigationGestures }}WebView滑动在页面历史中前进后退。
.header_right_system_item(..)ios={{ headerRightSystemItem }}NavPage标题栏末尾显示 UIKit 自带的按钮,按下即按下 headerRight。
.back_forward_gestures(..)desktop={{ allowsBackForwardNavigationGestures }}WebViewmacOS:双指在触控板上轻扫,在页面历史中前进后退;Windows:在触摸屏上轻扫;Linux:在触控板上轻扫。
.window_drag(..)desktop={{ windowDrag }}所有元素按住它拖动窗口,像系统标题栏一样(见 nsh/desktop)。

在 TSX 中,desktop={{…}} 和 web={{…}} 在所有地方都可以使用。类型(IOSProps、AndroidProps、DesktopProps 及其各元素的形式)会告诉编辑器每个元素接受什么。

两个及以上平台都有的功能是顶层属性(Rust 中是同名方法),在有意义的平台上生效:NavPage 的 title、headerStyle、headerLargeTitle 和 headerBackTitle 设置 iOS 和 Android 的系统标题栏,它的 header 插槽是桌面和 Web 上的标题栏,Dialog 的 presentation 选择 iOS 的表单样式。AlertDialog.show 的 closeOnBackdropPress、pagingEnabled、refreshing、hitSlop 等在所有平台上都可用。

#平台模块

在 Rust 中,只有一个平台才有的 API 是 nsh_core::prelude 的函数(on_back_press、input_accessory_view)和 nsh_core::element::desktop 的函数(set_title_bar、title_bar、close_window);在其他平台上它们什么都不做。在 TSX 中,nsh/mobile 存放 iOS 和 Android 的 API(其中一些只属于其中一个平台),nsh/desktop 存放桌面的 API。两者在所有平台上都可以导入,因此同一个包可以到处运行:在其他平台上,它们的函数会立即返回,组件不占空间。每个模块都导出 isAvailable,在自己的平台上为 true;platform.os 区分具体平台。

#nsh/mobile

isAvailable 在 iOS(包括 Mac Catalyst)和 Android 上为 true。两个平台都有系统标签栏 TabBar。

#BackHandler(Android)

RustTSX说明
on_back_press(listener)BackHandler.onBackPress(listener)在对话框之后、导航栈之前监听返回键;处理了就返回 true。监听器随添加它的组件(或 effect)一起结束;在 TSX 中,调用还会返回一个函数,用来提前停止监听。
BackHandler.exitApp()退出应用。
use nsh_core::prelude::*;

fn player() -> impl IntoElement {
    let full_screen = signal(false);
    // 返回键先退出全屏,再离开页面。监听器随视图一起结束。
    on_back_press(move || {
        if !full_screen.get_untracked() {
            return false;
        }
        full_screen.set(false);
        true
    });
    // …
}

返回键先由最上层的对话框处理,然后是返回键监听器(最近添加的最先),再然后是最上层的导航栈,都没有处理时应用退出。见导航。

#InputAccessoryView(iOS)

RustTSX说明
input_accessory_view(native_id)InputAccessoryView在引用其原生 id(.input_accessory(..)、ios={{ inputAccessoryViewID }})的输入框的键盘上方显示的工具栏。

见表单与键盘。

#nsh/desktop

Rust 应用用 nsh_core::element::desktop 绘制自己的标题栏(用 Rust 开发)。TSX 的模块:

导出说明
Window窗口:setTitle、setTitleBar("hidden")(自定义标题栏)、minimize、toggleMaximize、toggleFullScreen、close。
titleBar隐藏的标题栏的高度,以及系统窗口按钮占用的空间(响应式)。
MenusetApplicationMenu(items)(菜单栏,带快捷键)和 popup(items, at?)(右键菜单)。
isAvailable在 macOS、Windows 和 Linux 上为 true。

见 nsh/desktop。

#状态栏

在两个平台上,状态栏都属于最上层的页面或对话框,和 UIKit 中由最上层视图控制器决定一样:NavPage 和 Dialog 接受 statusBar(Rust 中为 .status_bar(StatusBarStyle::Light),其他字段用 StatusBarSetting),其中有 foreground("auto"、深色背景用 "light"、浅色背景用 "dark")、hidden 和 animated。最近显示的页面或对话框按字段逐项生效,它离开后下面那个重新生效。nsh.json 的 "statusBar" 是它们之下的应用默认值。

use nsh_base::nav_page;
use nsh_core::prelude::*;

fn photo_viewer() -> impl IntoElement {
    nav_page()
        .header_visible(false)
        .status_bar(StatusBarStyle::Light)
        .child(view().flex_1().bg(black()))
}

在嵌入原生应用的场景下,iOS 上由宿主应用的视图控制器决定状态栏。在 nsh.json 中声明了 "capabilities" 的应用,需要 "statusBar": true 才能修改它。

#platform.os 与 platform.select

Rust 中的 os() 和 TSX 中 nsh 的常量 platform 告诉你应用运行在哪里:Os::Ios、Os::Android、Os::MacOs、Os::Windows、Os::Linux 或 Os::Web,platform.os 为 "ios"、"android"、"macos"、"windows"、"linux" 或 "web"(Mac Catalyst 为 "ios",同时 platform.catalyst 为真;macOS 后端为 "macos",Linux 后端为 "linux",Windows 后端为 "windows",它们的元素会应用 desktop 属性;浏览器为 "web",界面风格为 "desktop" 时也会应用 desktop 属性)。版本、界面风格等见设备与环境。

use nsh_core::{element::Os, prelude::*};

if os() == Os::Android {
    // …
}

#platform.select

在 Rust 中用 match os() 选择值,桌面用 is_desktop()。在 TSX 中,platform.select(spec) 返回当前平台对应的值。它依次查找该操作系统自己的键,然后是 mobile(iOS、Android、手机和平板浏览器)或 desktop(macOS、Windows、Linux、桌面浏览器),再是 native(除 Web 外的所有平台),最后是 default:

use nsh_core::{element::Os, prelude::*};

fn card() -> View {
    let card = view().rounded(px(12.0)).bg(white());
    match os() {
        Os::Ios => card.shadow_md(),
        Os::Android => card.border_1().border_color(rgb(0xe5e7eb)),
        _ => card,
    }
}

let hint = match os() {
    Os::Ios => "Swipe down to close",
    Os::Android => "Press back to close",
    _ => "Close",
};

没有匹配项且没有 default 时返回 undefined。

#渲染不同的组件

组件也可以这样选择;选择永远不变时(TSX)放在模块顶层:

fn spinner() -> AnyElement {
    if os() == Os::Ios {
        ios_spinner().into_element()
    } else {
        material_spinner().into_element()
    }
}

或者用 when / <If> 内联:

when(os() == Os::Ios, || input_accessory_view("bar").child(…))
    .otherwise(|| text("Android has no input accessories."))

#Tailwind 中的平台变体

Rust 用 .when(os() == Os::Ios, |v| ..) 按平台设置样式;在 TSX 中,ios:、android: 和 web: 变体只在一个平台上应用对应的类:

view()
    .rounded_xl()
    .bg(white())
    .p_4()
    .when(os() == Os::Ios, |v| v.shadow_md())
    .when(os() == Os::Android, |v| v.border_1().border_color(rgb(0xe4e4e7)))

#优先使用共享的行为

在添加平台判断之前,先想想这个差异对用户是否重要。元素和样式已经在应当不同的地方映射到各平台的原生行为(滚动物理效果、文字渲染、键盘、无障碍),在用户期望一致的地方(你的应用自己的外观)保持一致。平台判断最好留给用户依赖的平台惯例:Android 的返回键和 toast、iOS 的表单弹窗和大标题、自动填充的名称、Android 的水波纹。

#相关内容