Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

Elementos

Image

Image muestra una imagen de los assets de la app o de una URL. Las imágenes se descargan y se decodifican fuera del hilo principal, se reducen al tamaño con el que la vista las muestra y se almacenan en caché en memoria. Las imágenes animadas GIF, WebP y APNG se reproducen. Rust crea una con image(src), TSX con <Image> desde 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(),
    )

Un asset conoce su tamaño: sin ancho ni alto, la imagen lo toma de él, y con uno de los dos el otro se deduce de la relación de aspecto del asset. El tamaño de una URL se conoce una vez que carga, así que da a las imágenes remotas un ancho y un alto (o una relación de aspecto junto con uno de ellos).

#Fuentes

La fuente (src y placeholder; image(..) y .placeholder(..) en Rust) acepta:

Valor Ejemplo Descripción
Un asset Rust asset!("logo.png"), TSX import logo from "./logo.png" Un ImageAsset { url, size, scales? }, con tamaño en puntos: un archivo @2x cuenta la mitad de sus píxeles. Con logo@2x.png / logo@3x.png junto a logo.png, se carga la variante correspondiente a la densidad de píxeles de la pantalla (densidades de píxeles). Se pueden importar .png, .jpg, .jpeg, .gif, .webp y .heic, y también .svg, con el tamaño dado por su width y height o viewBox (imágenes SVG).
Una URL "https://example.com/a.jpg" Se descarga y se almacena en caché. Las descargas de la misma URL se comparten entre vistas.
Una ruta de asset "asset:/images/a.png" Un archivo de los assets de la app.
Un URI data: "data:image/png;base64,iVBOR…" Bytes en el URI, en base64 o con codificación porcentual (data:image/svg+xml,%3Csvg…).
Una URL blob: URL.createObjectURL(blob) Los bytes de un Blob o File (TSX). La imagen los conserva: revocar la URL deja una imagen que se muestra completa.

Una imagen creada a partir de bytes que la app ya tiene se muestra sin hacer una petición. Un chat muestra una foto elegida desde su blob mientras se sube, y después cambia src a la URL de la foto:

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

Los bytes iguales son una sola imagen: la misma imagen como URI data: y como blob se decodifica una vez y comparte la caché.

Durante el desarrollo, los assets los sirve el servidor de desarrollo; nsh build y nsh package los copian en la app (un crate de Rust incluye su directorio assets/ con registry.assets(assets!())). Consulta imágenes y assets.

#Props

Además de las props comunes (con style tipado como ImageStyle):

Prop Rust Tipo Valor por defecto Descripción
src image(src) string | ImageAsset La imagen.
placeholder .placeholder(src) string | ImageAsset Se muestra hasta que src se haya cargado; lo mejor es un asset local.
blurRadius .blur_radius(px(..)) number Desenfoca la imagen con este radio en puntos (se aplica una sola vez durante la decodificación, fuera del hilo principal).
fadeDuration .fade_duration(ms) number iOS 0, Android 300 Cuánto dura, en milisegundos, el fundido de entrada de una imagen cargada.
tintColor (estilo) .tint_color(..) ColorValue Cambia el color de una imagen de plantilla (un icono dibujado en un solo color).
objectFit (estilo) .object_fit(ResizeMode::Contain) "cover" | "contain" | "fill" | "none" | "scale-down" "cover" Cómo se ajusta la imagen a su caja, como el object-fit de CSS (clases object-*): "cover" la llena y recorta, "contain" la ajusta dentro, "fill" la estira, "none" la muestra a su tamaño natural, "scale-down" lo mismo, reducida para caber donde es más grande.
objectPosition (estilo) .object_position(..), .object_top(), … string "center" Dónde se sitúa la imagen en su caja una vez que objectFit la ha dimensionado, como el object-position de CSS ("top", "25% 75%", "right 10px bottom 20%"); una imagen que cubre se recorta alrededor de ese punto.
onLoadStart .on_load_start(..) () => void Comenzó la carga.
onProgress .on_progress(..) (e: { loaded: number; total: number }) => void Progreso de la descarga en bytes (total es 0 cuando se desconoce), como máximo cada 100 ms.
onLoad .on_load(..) (e: { size: PixelSize }) => void Cargada; el tamaño de la imagen original en píxeles (no el de la decodificada y reducida; el tamaño de un SVG en puntos, en la web tal como lo mide el navegador); para una imagen con variantes, el de la variante cargada.
onError .on_error(..) (e: { message: string; code?: string }) => void La carga falló; code es "ERR_ACCESS_DENIED" para una URL que las capabilities de la app no permiten.
onLoadEnd .on_load_end(..) () => void Después de onLoad o onError.

ImageStyle añade tintColor a los estilos de la vista, que cambia el color de una imagen de plantilla (un icono dibujado en un solo color), y objectPosition, que sitúa la imagen en su caja (Posición de la imagen).

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

#Eventos

Evento Rust Payload Cuándo
onLoadStart .on_load_start ninguno Comenzó la carga.
onProgress .on_progress { loaded, total } (ImageProgress) Llegaron bytes (como máximo cada 100 ms).
onLoad .on_load { size } (Size<DevicePixels>) La imagen se muestra; su tamaño original en píxeles (el de un SVG en puntos, en la web tal como lo mide el navegador).
onError .on_error { message, code? } (&str, el mensaje) La carga o la decodificación falló, o el host queda fuera de las capabilities de la app (code "ERR_ACCESS_DENIED").
onLoadEnd .on_load_end ninguno Después de onLoad o 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()))

Una imagen con variantes de densidad de píxeles carga uno de sus archivos, y onLoad informa de los píxeles de ese archivo: 192×192 para logo@3x.png en una pantalla 3×, para un logo.png de 64×64 puntos. Cuando la densidad de píxeles cambia y la imagen carga otra variante, también informa de esa carga: onLoadStart, luego onLoad con el tamaño del nuevo archivo (o onError), y después onLoadEnd.

Para conocer el tamaño de una imagen o cargarla antes de mostrarla, TSX llama a Image.getSize(src) y Image.prefetch(src) (el mismo Image que renderiza el elemento los incluye; consulta la API de Image), y Rust usa image_size(src) y prefetch_image(src), con await en una tarea. Dada una imagen importada con variantes, Image.getSize y Image.prefetch cargan la variante que muestra esta pantalla, y getSize devuelve el tamaño del archivo importado (64×64 para logo.png).

#Métodos de ref

Ninguno propio.

#Notas de plataforma

  • La clave de la caché incluye el tamaño de decodificación y el radio de desenfoque: la misma URL mostrada en dos tamaños se decodifica dos veces, pero se descarga una sola.
  • iOS reproduce las imágenes animadas como una animación de fotogramas clave (hasta 600 fotogramas); una imagen animada con tinte muestra su primer fotograma.
  • Android reproduce las imágenes animadas desde la API 28 (AnimatedImageDrawable), sin caché y sin desenfoque.
  • En Android los bordes se dibujan por encima de la imagen, igual que en iOS.