元素
Image
Image 显示来自应用资源或 URL 的图片。图片在主线程之外下载和解码,按视图显示的尺寸降采样,并缓存在内存中。动态 GIF、WebP 和 APNG 图片会自动播放。Rust 用 image(src) 创建,TSX 用从 nsh 导入的 <Image>。
import { Image, mount, signal, Text, View } from "nsh";
import { windowSize } from "nsh";
import { button, buttonText, focusRing, ui } from "./palette";
// The pictures are SVG `data:` URIs, so the demo needs no network. An app
// would import a file next to it (`import hills from "./hills.png"`) or pass
// a URL.
const svg = (body: string, width: number, height: number) =>
`data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${body}</svg>`)}`;
const landscape = svg(
`<defs><linearGradient id="s" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#7ea8ff"/><stop offset="1" stop-color="#ffd59e"/></linearGradient></defs>
<rect width="1200" height="800" fill="url(#s)"/>
<circle cx="880" cy="250" r="90" fill="#fff4c2"/>
<path d="M0 560 Q 250 380 520 520 T 1200 470 V800 H0Z" fill="#6aa86b"/>
<path d="M0 650 Q 330 520 700 640 T 1200 610 V800 H0Z" fill="#3f7d4c"/>
<text x="40" y="770" font-family="system-ui" font-size="40" fill="#ffffff">1200 × 800</text>`,
1200,
800,
);
const portrait = svg(
`<defs><linearGradient id="n" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1b1f3a"/><stop offset="1" stop-color="#b79cff"/></linearGradient></defs>
<rect width="600" height="900" fill="url(#n)"/>
<circle cx="300" cy="260" r="120" fill="#f4f1ff"/>
<circle cx="350" cy="230" r="110" fill="#2a2f55"/>
<path d="M0 700 L 180 520 L 330 660 L 450 560 L 600 700 V900 H0Z" fill="#141629"/>
<text x="30" y="870" font-family="system-ui" font-size="36" fill="#ffffff">600 × 900</text>`,
600,
900,
);
// Shown until the picture loads, and in its place when it fails.
const placeholder = svg(
`<rect width="120" height="80" fill="#8d9198" opacity="0.25"/><path d="M20 62 L48 34 L68 52 L80 42 L100 62Z" fill="#8d9198"/><circle cx="86" cy="24" r="8" fill="#8d9198"/>`,
120,
80,
);
// An icon drawn in one color: `tintColor` recolors it.
const star = svg(`<path d="M12 2l3 6.5 7 .8-5.2 4.8 1.5 7L12 17.6 5.7 21.1l1.5-7L2 9.3l7-.8z" fill="#000"/>`, 24, 24);
const sources = { landscape, portrait, missing: "asset:/missing.png" } as const;
type Source = keyof typeof sources;
const modes = ["cover", "contain", "stretch", "center"] as const;
type Mode = (typeof modes)[number];
function Segments<T extends string>(props: { options: readonly T[]; value: T; onChange: (v: T) => void }) {
return (
<View style={{ ...ui().inset, flexDirection: "row", padding: 3, borderRadius: 10 }}>
<Each of={props.options}>
{(option) => {
const on = () => props.value === option();
return (
<View
onPress={() => props.onChange(option())}
style={{
flex: 1,
alignItems: "center",
paddingVertical: 6,
borderRadius: 8,
backgroundColor: on() ? ui().raised.backgroundColor : "transparent",
backgroundImage: on() ? ui().raised.backgroundImage : "none",
boxShadow: on() ? ui().raised.boxShadow : "none",
}}
hoverStyle={{ opacity: 0.88 }}
focusStyle={focusRing()}
activeStyle={{ opacity: 0.7 }}
>
<Text style={{ fontWeight: "600", color: on() ? ui().text : ui().muted }}>{option()}</Text>
</View>
);
}}
</Each>
</View>
);
}
function App() {
const source = signal<Source>("landscape");
const mode = signal<Mode>("cover");
const blur = signal(false);
const events = signal<string[]>([]);
const log = (line: string) => events.set([line, ...events()].slice(0, 4));
const wide = () => windowSize().width >= 700;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>image</Text>
<View style={{ flex: 1, flexDirection: wide() ? "row" : "column", gap: 16 }}>
<Image
src={sources[source()]}
placeholder={placeholder}
resizeMode={mode()}
blurRadius={blur() ? 12 : 0}
fadeDuration={250}
onLoadStart={() => log("onLoadStart")}
onLoad={(e) => log(`onLoad ${Math.round(e.width)} × ${Math.round(e.height)}`)}
onError={(e) => log(`onError ${e.message}`)}
onLoadEnd={() => log("onLoadEnd")}
style={{ ...ui().inset, flex: 1, minHeight: 180, borderRadius: 14 }}
/>
<View style={{ gap: 12, width: wide() ? 320 : undefined }}>
<Text style={{ color: ui().muted }}>src</Text>
<Segments options={Object.keys(sources) as Source[]} value={source()} onChange={(s) => source.set(s)} />
<Text style={{ color: ui().muted }}>resizeMode</Text>
<Segments options={modes} value={mode()} onChange={(m) => mode.set(m)} />
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<View onPress={() => blur.set(!blur())} {...button("secondary", { on: blur })}>
<Text style={buttonText("secondary", { on: blur })}>blurRadius {blur() ? 12 : 0}</Text>
</View>
<Text style={{ color: ui().muted }}>tintColor</Text>
<Image src={star} style={{ width: 24, height: 24, tintColor: ui().accent }} />
<Image src={star} style={{ width: 24, height: 24, tintColor: ui().info }} />
</View>
<View style={{ ...ui().card, borderRadius: 12, padding: 12, gap: 4 }}>
<Text style={{ color: ui().muted, fontSize: 12 }}>Events, newest first</Text>
<Each of={events()}>
{(line) => <Text style={{ color: ui().text, fontFamily: "monospace", fontSize: 13 }}>{line()}</Text>}
</Each>
</View>
</View>
</View>
</View>
);
}
mount(App);use nsh_core::prelude::*;
// 应用 crate 中的 assets/logo.png。
const LOGO: ImageAsset = asset!("logo.png");
view()
.flex_row()
.gap_3()
.child(image(LOGO))
.child(
image("https://example.com/avatar.jpg")
.placeholder(LOGO)
.resize_mode(ResizeMode::Cover)
.size(px(64.0))
.rounded_full(),
)import { Image, View } from "nsh";
import logo from "./assets/logo.png";
<View style={{ flexDirection: "row", gap: 12 }}>
<Image src={logo} />
<Image
src="https://example.com/avatar.jpg"
placeholder={logo}
resizeMode="cover"
style={{ width: 64, height: 64, borderRadius: 32 }}
/>
</View>;资源知道自己的尺寸:不设宽高时图片使用该尺寸;只设其中一个时,另一个按资源的宽高比计算。URL 的尺寸在加载完成后才知道,所以要给远程图片设置宽和高(或者其中一个加上宽高比)。
#图片来源
图片来源(src 和 placeholder;Rust 中为 image(..) 和 .placeholder(..))接受:
| 值 | 示例 | 说明 |
|---|---|---|
| 资源 | Rust asset!("logo.png"),TSX import logo from "./logo.png" | 一个 ImageAsset { url, size, scales? },以点为单位:@2x 文件按像素的一半计算。当 logo.png 旁边有 logo@2x.png / logo@3x.png 时,会加载适合屏幕像素比的变体(像素密度)。可以导入 .png、.jpg、.jpeg、.gif、.webp 和 .heic,以及按 width 和 height 或 viewBox 确定尺寸的 .svg(SVG 图片)。 |
| URL | "https://example.com/a.jpg" | 下载并缓存。同一 URL 的下载在多个视图之间共享。 |
| 资源路径 | "asset:/images/a.png" | 应用资源中的文件。 |
data: URI | "data:image/png;base64,iVBOR…" | URI 中的字节,base64 或百分号编码(data:image/svg+xml,%3Csvg…)。 |
blob: URL | URL.createObjectURL(blob) | Blob 或 File 的字节(TSX)。图片保有这些字节:撤销 URL 后,正在显示它的图片保持完整。 |
来自应用已有字节的图片无需请求即可显示。聊天应用在照片上传期间用它的 blob 显示所选照片,上传完成后把 src
换成照片的 URL:
// `data:` URI 中的字节(应用生成或内联收到的图片)。
image(format!("data:image/png;base64,{encoded}")).size(px(64.0))import { Image, signal } from "nsh";
const preview = URL.createObjectURL(photo); // 所选的 `File`
const src = signal(preview);
upload(photo).then((url) => {
src.set(url);
URL.revokeObjectURL(preview);
});
<Image src={src()} style={{ width: 240, height: 180 }} />;相同的字节是同一张图片:同一张图片无论作为 data: URI 还是 blob,都只解码一次并共享缓存。
开发期间资源由开发服务器提供;nsh build 和 nsh package 会把它们拷贝进应用(Rust crate 用 registry.assets(assets!()) 发布它的 assets/ 目录)。参见图片与资源。
#属性
除通用属性外(style 的类型为 ImageStyle):
| 属性 | Rust | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
src | image(src) | string | ImageAsset | 图片。 | |
resizeMode | .resize_mode(ResizeMode::Contain) | "cover" | "contain" | "stretch" | "center" | "scale-down" | "cover" | 图片如何适配其盒子:"center" 以原始尺寸居中显示(CSS 的 object-fit: none);"scale-down" 同样居中,较大时缩小以适应。 |
placeholder | .placeholder(src) | string | ImageAsset | src 加载完成前显示的图片;最好是本地资源。 | |
blurRadius | .blur_radius(px(..)) | number | 以该半径(点)模糊图片(在解码时于主线程外处理一次)。 | |
fadeDuration | .fade_duration(ms) | number | iOS 0,Android 300 | 图片加载完成后淡入的时长,单位毫秒。 |
tintColor(样式) | .tint_color(..) | ColorValue | 为模板图片(单色绘制的图标)重新着色。 | |
objectPosition(样式) | .object_position(..)、.object_top() 等 | string | "center" | resizeMode 确定尺寸后图片在盒子中的位置,同 CSS 的 object-position("top"、"25% 75%"、"right 10px bottom 20%");覆盖盒子的图片围绕它裁剪。 |
onLoadStart | .on_load_start(..) | () => void | 开始加载。 | |
onProgress | .on_progress(..) | (e: { loaded: number; total: number }) => void | 以字节计的下载进度(未知时 total 为 0),最多每 100 ms 一次。 | |
onLoad | .on_load(..) | (e: { width: number; height: number }) => void | 加载完成;给出源图片的像素尺寸(不是解码、降采样后的尺寸;SVG 为它的点尺寸,在 Web 上为浏览器测得的尺寸);带有变体的图片给出所加载变体的尺寸。 | |
onError | .on_error(..) | (e: { message: string }) => void | 加载失败。 | |
onLoadEnd | .on_load_end(..) | () => void | 在 onLoad 或 onError 之后。 |
ImageStyle 在视图样式之上增加了 tintColor,为模板图片(单色绘制的图标)重新着色;以及 objectPosition,决定图片在盒子中的位置(图片位置)。
image(ICON).size(px(24.0)).tint_color(rgb(0x0969da))<Image src={icon} style={{ width: 24, height: 24, tintColor: "#0969da" }} />#事件
| 事件 | Rust | 载荷 | 触发时机 |
|---|---|---|---|
onLoadStart | .on_load_start | 无 | 开始加载。 |
onProgress | .on_progress | { loaded, total }(ImageProgress) | 收到数据(最多每 100 ms 一次)。 |
onLoad | .on_load | { width, height }(Size<Points>) | 图片已显示;给出源图片的像素尺寸(SVG 为点,在 Web 上为浏览器测得的尺寸)。 |
onError | .on_error | { message }(&str) | 加载或解码失败,或主机不在应用的能力范围内。 |
onLoadEnd | .on_load_end | 无 | 在 onLoad 或 onError 之后。 |
let state = signal(SharedString::from("加载中"));
image(url)
.w_full()
.aspect_ratio(16.0 / 9.0)
.on_load(move |size: &Size<Points>| state.set(format!("{}×{}", size.width.0, size.height.0).into()))
.on_error(move |message: &str| state.set(message.to_string().into()))const state = signal("加载中");
<Image
src={url}
style={{ width: "100%", aspectRatio: 16 / 9 }}
onLoad={(e) => state.set(`${e.width}×${e.height} px`)}
onError={(e) => state.set(e.message)}
/>;带有像素密度变体的图片会加载其中一个文件,onLoad 报告的是该文件的像素尺寸:对于 64×64 点的 logo.png,在 3× 屏幕上报告 logo@3x.png 的 192×192。像素比变化、图片加载另一个变体时,这次加载同样会触发事件:先是 onLoadStart,然后是带有新文件尺寸的 onLoad(或 onError),最后是 onLoadEnd。
如果需要在显示前获取图片尺寸或预先加载,TSX 调用 Image.getSize(src) 和 Image.prefetch(src)(它们就挂在渲染这个元素的同一个 Image 上;参见 Image API),Rust 在任务中 await image_size(src) 和 prefetch_image(src)。
传入带有变体的导入图片时,Image.getSize 和 Image.prefetch 加载当前屏幕显示的那个变体,getSize resolve 为导入文件的尺寸(logo.png 为 64×64)。
#ref 方法
没有自己的方法。
#平台说明
- 缓存键包含解码尺寸和模糊半径:同一 URL 以两种尺寸显示会解码两次,但只下载一次。
- iOS 以关键帧动画播放动态图片(最多 600 帧);着色的动态图片显示第一帧。
- Android 从 API 28 起播放动态图片(
AnimatedImageDrawable),这些图片直接播放,不经缓存和模糊。 - 在 Android 上边框绘制在图片之上,与 iOS 一致。