元素
Image
Image 顯示來自應用程式資源或 URL 的圖片。圖片會在主執行緒以外下載和解碼,按檢視實際顯示的大小縮小取樣,並快取在記憶體中。支援播放動態 GIF、WebP 和 APNG 圖片。Rust 以 image(src) 建立,TSX 則以 <Image> 配合 nsh 建立。
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" |
應用程式資源中的檔案。 |
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:
// 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 會將資源複製到應用程式中(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 |
下載進度(以位元組計),最多每 100 毫秒一次(大小未知時 total 為 0)。 |
|
onLoad |
.on_load(..) |
(e: { size: PixelSize }) => void |
載入完成;來源圖片的大小(以像素計,不是解碼並縮小取樣後的大小;SVG 的大小以點計,在網頁上則由瀏覽器量度);如果圖片有多個版本,則為所載入版本的大小。 | |
onError |
.on_error(..) |
(e: { message: string; code?: string }) => void |
載入失敗;如果 URL 未獲應用程式的 capabilities 授權,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 以點計,在網頁上則由瀏覽器量度)。 |
onError |
.on_error |
{ message, code? }(&str,即訊息) |
載入或解碼失敗,或主機不在應用程式的 capabilities 範圍內(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× 螢幕上,對 64×64 點的 logo.png 而言,logo@3x.png 為 192×192。當像素比例改變而圖片載入另一個版本時,也會回報該次載入: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 相同。