元素
Image
Image 可顯示來自 App 資產或 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", "fill", "none"] 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}
blurRadius={blur() ? 12 : 0}
fadeDuration={250}
onLoadStart={() => log("onLoadStart")}
onLoad={({ size }) => log(`onLoad ${Math.round(size.width)} × ${Math.round(size.height)}`)}
onError={(e) => log(`onError ${e.message}`)}
onLoadEnd={() => log("onLoadEnd")}
style={{ ...ui().inset, flex: 1, minHeight: 180, borderRadius: 14, objectFit: mode() }}
/>
<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 }}>objectFit</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::*;
// assets/logo.png in the app's crate.
const LOGO: ImageAsset = asset!("logo.png");
view()
.flex_row()
.gap_3()
.child(image(LOGO))
.child(
image("https://example.com/avatar.jpg")
.placeholder(LOGO)
.object_fit(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}
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? },以點(point)為單位計算大小:@2x 檔案的尺寸為其像素的一半。若在 logo.png 旁有 logo@2x.png / logo@3x.png,則會載入對應螢幕像素比例的版本(像素密度)。可匯入 .png、.jpg、.jpeg、.gif、.webp 與 .heic,也可匯入 .svg,其大小由 width 與 height 或 viewBox 決定(SVG 圖片)。 |
| URL | "https://example.com/a.jpg" | 下載並快取。相同 URL 的下載會在各檢視之間共用。 |
| 資產路徑 | "asset:/images/a.png" | App 資產中的檔案。 |
data: URI | "data:image/png;base64,iVBOR…" | URI 中的位元組,以 base64 或百分比編碼(data:image/svg+xml,%3Csvg…)。 |
blob: URL | URL.createObjectURL(blob) | Blob 或 File(TSX)的位元組。圖片會保留這些位元組:即使撤銷該 URL,圖片仍會完整顯示。 |
App 已持有的位元組可直接顯示圖片,無需發出請求。聊天室在上傳時會先以 blob 顯示使用者挑選的照片,上傳完成後再將 src 切換為該照片的 URL:
// Bytes in a `data:` URI (an image the app made or received inline).
image(format!("data:image/png;base64,{encoded}")).size(px(64.0))import { Image, signal } from "nsh";
const preview = URL.createObjectURL(photo); // the picked `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 會將資產複製到 App 中(Rust crate 則以 registry.assets(assets!()) 隨附其 assets/ 目錄)。請參閱圖片與資產。
#屬性
除了通用屬性之外(其中 style 的型別為 ImageStyle):
| 屬性 | Rust | 型別 | 預設值 | 說明 |
|---|---|---|---|---|
src | image(src) | string | ImageAsset | 圖片。 | |
placeholder | .placeholder(src) | string | ImageAsset | 在 src 載入完成前顯示;最好使用本機資產。 | |
blurRadius | .blur_radius(px(..)) | number | 以此半徑(單位為點)模糊圖片(在解碼時於主執行緒之外套用一次)。 | |
fadeDuration | .fade_duration(ms) | number | iOS 0,Android 300 | 載入完成的圖片淡入所需的時間,單位為毫秒。 |
tintColor(樣式) | .tint_color(..) | ColorValue | 為範本圖片(以單一顏色繪製的圖示)重新著色。 | |
objectFit(樣式) | .object_fit(ResizeMode::Contain) | "cover" | "contain" | "fill" | "none" | "scale-down" | "cover" | 圖片如何符合其方框,與 CSS 的 object-fit 相同(object-* 類別):"cover" 填滿並裁切,"contain" 完整容納於其中,"fill" 拉伸,"none" 以原始尺寸顯示,"scale-down" 同上,但若圖片較大則縮小至符合。 |
objectPosition(樣式) | .object_position(..)、.object_top()、… | string | "center" | 在 objectFit 決定尺寸後,圖片在其方框中的位置,與 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 毫秒回報一次。 | |
onLoad | .on_load(..) | (e: { size: PixelSize }) => void | 載入完成;為來源圖片以像素表示的尺寸(不是解碼後縮小取樣的尺寸;SVG 則為以點表示的尺寸,在 Web 上以瀏覽器量測的結果為準);若圖片有多個變體,則為所載入變體的尺寸。 | |
onError | .on_error(..) | (e: { message: string; code?: string }) => void | 載入失敗;對於 App 權限未授權的 URL,code 為 "ERR_ACCESS_DENIED"。 | |
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 毫秒一次)。 |
onLoad | .on_load | { size }(Size<DevicePixels>) | 圖片已顯示;為其來源以像素表示的尺寸(SVG 則以點表示,在 Web 上以瀏覽器量測的結果為準)。 |
onError | .on_error | { message, code? }(&str,即訊息) | 載入或解碼失敗,或主機不在 App 的權限範圍內(code "ERR_ACCESS_DENIED")。 |
onLoadEnd | .on_load_end | 無 | 在 onLoad 或 onError 之後。 |
let state = signal(SharedString::from("loading"));
image(url)
.w_full()
.aspect_ratio(16.0 / 9.0)
.on_load(move |size: &Size<DevicePixels>| state.set(format!("{}×{}", size.width.0, size.height.0).into()))
.on_error(move |message: &str| state.set(message.to_string().into()))const state = signal("loading");
<Image
src={url}
style={{ width: "100%", aspectRatio: 16 / 9 }}
onLoad={({ size }) => state.set(`${size.width}×${size.height} px`)}
onError={(e) => state.set(e.message)}
/>;具有像素密度變體的圖片會載入其中一個檔案,而 onLoad 回報的是該檔案的像素:在 3× 螢幕上,logo@3x.png 為 192×192,對應 64×64 點的 logo.png。當像素比例改變而圖片載入另一個變體時,同樣會回報該次載入:先是 onLoadStart,接著是帶有新檔案尺寸的 onLoad(或 onError),最後是 onLoadEnd。
若要在顯示圖片之前取得其尺寸或預先載入,TSX 可呼叫 Image.getSize(src) 與 Image.prefetch(src)(渲染該元素的同一個 Image 也提供這些方法;請參閱 Image API),Rust 則使用 image_size(src) 與 prefetch_image(src),並在任務中以 await 等待。對於具有變體的匯入圖片,Image.getSize 與 Image.prefetch 會載入此螢幕所顯示的變體,而 getSize 會以匯入檔案的尺寸解析(logo.png 為 64×64)。
#Ref 方法
無自有方法。
#平台注意事項
- 快取鍵包含解碼尺寸與模糊半徑:同一個 URL 以兩種尺寸顯示時,會解碼兩次,但只下載一次。
- iOS 以關鍵影格動畫播放動態圖片(最多 600 個影格);套用色調的動態圖片只會顯示第一個影格。
- Android 自 API 28(
AnimatedImageDrawable)起播放動態圖片,且不快取、不模糊。 - 在 Android 上,邊框繪製於圖片之上,與 iOS 相同。