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

元素

Image

Image 可顯示來自 App 資產或 URL 的圖片。圖片會在主執行緒之外下載與解碼,縮小取樣至檢視實際顯示的大小,並快取在記憶體中。支援播放動態 GIF、WebP 與 APNG。Rust 使用 image(src) 建立,TSX 則使用來自 nsh 的 <Image>。

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"App 資產中的檔案。
data: URI"data:image/png;base64,iVBOR…"URI 中的位元組,以 base64 或百分比編碼(data:image/svg+xml,%3Csvg…)。
blob: URLURL.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))

相同的位元組視為同一張圖片:同一張圖片無論以 data: URI 或 blob 提供,都只會解碼一次並共用快取。

開發期間,資產由開發伺服器提供;nsh build 與 nsh package 會將資產複製到 App 中(Rust crate 則以 registry.assets(assets!()) 隨附其 assets/ 目錄)。請參閱圖片與資產。

#屬性

除了通用屬性之外(其中 style 的型別為 ImageStyle):

屬性Rust型別預設值說明
srcimage(src)string | ImageAsset圖片。
placeholder.placeholder(src)string | ImageAsset在 src 載入完成前顯示;最好使用本機資產。
blurRadius.blur_radius(px(..))number以此半徑(單位為點)模糊圖片(在解碼時於主執行緒之外套用一次)。
fadeDuration.fade_duration(ms)numberiOS 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))

#事件

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

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