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

元素

Image

Image 顯示來自應用程式資源或 URL 的圖片。圖片會在主執行緒以外下載和解碼,按檢視實際顯示的大小縮小取樣,並快取在記憶體中。支援播放動態 GIF、WebP 和 APNG 圖片。Rust 以 image(src) 建立,TSX 則以 <Image> 配合 nsh 建立。

el-image.tsx
el-image.tsxLive
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(),
    )

資源本身已知道自己的大小:沒有指定寬度或高度時,圖片會採用資源的大小;只指定其中一項時,另一項會按資源的長寬比自動推算。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))

相同的位元組只算作一張圖片:同一幅圖片無論以 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))

#事件

事件 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()))

具有像素密度版本的圖片會載入其中一個檔案,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 相同。