Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

Elemente

Image

Image zeigt ein Bild aus den Assets der App oder von einer URL. Bilder werden außerhalb des Main-Threads heruntergeladen und dekodiert, auf die Größe heruntergerechnet, in der die View sie anzeigt, und im Speicher gecacht. Animierte GIF-, WebP- und APNG-Bilder werden abgespielt. In Rust erzeugt man eines mit image(src), in TSX mit <Image> aus 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(),
    )

Ein Asset kennt seine Größe: Ohne Breite oder Höhe übernimmt das Bild sie, und wird nur eines von beiden angegeben, ergibt sich das andere aus dem Seitenverhältnis des Assets. Die Größe einer URL ist erst nach dem Laden bekannt, daher sollten Remote-Bilder eine Breite und eine Höhe erhalten (oder ein Seitenverhältnis zusammen mit einem von beiden).

#Quellen

Die Quelle (src und placeholder; image(..) und .placeholder(..) in Rust) akzeptiert:

Wert Beispiel Beschreibung
Ein Asset Rust asset!("logo.png"), TSX import logo from "./logo.png" Ein ImageAsset { url, size, scales? }, in Punkten bemessen: Eine @2x-Datei zählt die Hälfte ihrer Pixel. Liegen logo@2x.png / logo@3x.png neben logo.png, wird die Variante für das Pixelverhältnis des Bildschirms geladen (Pixeldichten). .png, .jpg, .jpeg, .gif, .webp und .heic können importiert werden, ebenso .svg, dessen Größe sich aus width und height oder viewBox ergibt (SVG-Bilder).
Eine URL "https://example.com/a.jpg" Wird heruntergeladen und gecacht. Downloads derselben URL werden zwischen Views geteilt.
Ein Asset-Pfad "asset:/images/a.png" Eine Datei aus den Assets der App.
Ein data:-URI "data:image/png;base64,iVBOR…" Bytes im URI, base64- oder prozentkodiert (data:image/svg+xml,%3Csvg…).
Eine blob:-URL URL.createObjectURL(blob) Die Bytes eines Blob oder File (TSX). Das Bild behält sie: Wird die URL widerrufen, zeigt das Bild sich weiterhin vollständig.

Ein Bild aus Bytes, die die App bereits hat, wird ohne Anfrage angezeigt. Ein Chat zeigt ein ausgewähltes Foto aus seinem Blob an, während es hochgeladen wird, und stellt dann src auf die URL des Fotos um:

// Bytes in a `data:` URI (an image the app made or received inline).
image(format!("data:image/png;base64,{encoded}")).size(px(64.0))

Gleiche Bytes sind ein Bild: Dasselbe Bild als data:-URI und als Blob wird nur einmal dekodiert und teilt sich den Cache.

Während der Entwicklung werden Assets vom Dev-Server ausgeliefert; nsh build und nsh package kopieren sie in die App (ein Rust-Crate liefert sein assets/-Verzeichnis mit registry.assets(assets!()) aus). Siehe Bilder und Assets.

#Props

Neben den allgemeinen Props (mit style als ImageStyle typisiert):

Prop Rust Typ Standard Beschreibung
src image(src) string | ImageAsset Das Bild.
placeholder .placeholder(src) string | ImageAsset Wird angezeigt, bis src geladen ist; am besten ein lokales Asset.
blurRadius .blur_radius(px(..)) number Weichzeichnet das Bild mit diesem Radius in Punkten (einmal beim Dekodieren angewendet, außerhalb des Main-Threads).
fadeDuration .fade_duration(ms) number iOS 0, Android 300 Wie lange ein geladenes Bild eingeblendet wird, in Millisekunden.
tintColor (Style) .tint_color(..) ColorValue Färbt ein Template-Bild um (ein Icon in einer Farbe).
objectFit (Style) .object_fit(ResizeMode::Contain) "cover" | "contain" | "fill" | "none" | "scale-down" "cover" Wie das Bild in seine Box passt, wie CSS’ object-fit (object-*-Klassen): "cover" füllt sie und beschneidet, "contain" passt hinein, "fill" streckt, "none" zeigt es in natürlicher Größe, "scale-down" ebenso, aber verkleinert, wo es größer ist.
objectPosition (Style) .object_position(..), .object_top(), … string "center" Wo das Bild in seiner Box sitzt, nachdem objectFit es dimensioniert hat, wie CSS’ object-position ("top", "25% 75%", "right 10px bottom 20%"); ein füllendes Bild wird um diese Stelle herum beschnitten.
onLoadStart .on_load_start(..) () => void Der Ladevorgang hat begonnen.
onProgress .on_progress(..) (e: { loaded: number; total: number }) => void Download-Fortschritt in Bytes (total ist 0, wenn unbekannt), höchstens alle 100 ms.
onLoad .on_load(..) (e: { size: PixelSize }) => void Geladen; die Größe des Quellbilds in Pixeln (nicht des dekodierten, heruntergerechneten; bei einer SVG die Größe in Punkten, im Web so, wie der Browser sie misst); bei einem Bild mit Varianten die der geladenen Variante.
onError .on_error(..) (e: { message: string; code?: string }) => void Laden fehlgeschlagen; code ist "ERR_ACCESS_DENIED" bei einer URL, die die Capabilities der App nicht erlauben.
onLoadEnd .on_load_end(..) () => void Nach onLoad oder onError.

ImageStyle ergänzt die View-Styles um tintColor, das ein Template-Bild umfärbt (ein Icon in einer Farbe), und um objectPosition, das das Bild in seiner Box platziert (Bildposition).

image(ICON).size(px(24.0)).tint_color(rgb(0x0969da))

#Events

Event Rust Payload Wann
onLoadStart .on_load_start keine Der Ladevorgang hat begonnen.
onProgress .on_progress { loaded, total } (ImageProgress) Es sind Bytes angekommen (höchstens alle 100 ms).
onLoad .on_load { size } (Size<DevicePixels>) Das Bild wird angezeigt; seine Quellgröße in Pixeln (bei einer SVG in Punkten, im Web so, wie der Browser sie misst).
onError .on_error { message, code? } (&str, die Meldung) Laden oder Dekodieren ist fehlgeschlagen, oder der Host liegt außerhalb der Capabilities der App (code "ERR_ACCESS_DENIED").
onLoadEnd .on_load_end keine Nach onLoad oder 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()))

Ein Bild mit Pixeldichte-Varianten lädt eine seiner Dateien, und onLoad meldet die Pixel dieser Datei: 192×192 für logo@3x.png auf einem 3×-Bildschirm, bei einem logo.png mit 64×64 Punkten. Ändert sich das Pixelverhältnis und lädt das Bild eine andere Variante, meldet es auch dieses Laden: onLoadStart, dann onLoad mit der Größe der neuen Datei (oder onError), dann onLoadEnd.

Um die Größe eines Bildes zu erfahren oder es vor dem Anzeigen zu laden, ruft TSX Image.getSize(src) und Image.prefetch(src) auf (dasselbe Image, das das Element rendert, trägt sie; siehe die Image API), und Rust image_size(src) und prefetch_image(src), mit await in einem Task. Bei einem importierten Bild mit Varianten laden Image.getSize und Image.prefetch die Variante, die dieser Bildschirm zeigt, und getSize liefert die Größe der importierten Datei (64×64 für logo.png).

#Ref-Methoden

Keine eigenen.

#Plattformhinweise

  • Der Cache-Schlüssel enthält die Dekodiergröße und den Weichzeichnungsradius: Dieselbe URL, in zwei Größen angezeigt, wird zweimal dekodiert, aber nur einmal heruntergeladen.
  • iOS spielt animierte Bilder als Keyframe-Animation ab (bis zu 600 Frames); ein eingefärbtes animiertes Bild zeigt seinen ersten Frame.
  • Android spielt animierte Bilder ab API 28 ab (AnimatedImageDrawable), ungecacht und ohne Weichzeichnung.
  • Unter Android werden Rahmen über dem Bild gezeichnet, wie unter iOS.