Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Elementos

Image

Image mostra uma imagem dos assets do app ou de uma URL. As imagens são baixadas e decodificadas fora da thread principal, reduzidas ao tamanho em que a view as exibe e armazenadas em cache na memória. Imagens animadas GIF, WebP e APNG são reproduzidas. Em Rust, cria-se uma com image(src); em TSX, com <Image> a partir de 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(),
    )

Um asset conhece seu próprio tamanho: sem largura nem altura, a imagem adota esse tamanho, e, com apenas uma das duas, a outra acompanha a proporção do asset. O tamanho de uma URL só é conhecido depois que ela carrega, então defina largura e altura para imagens remotas (ou uma proporção junto com uma das duas).

#Fontes

A fonte (src e placeholder; image(..) e .placeholder(..) em Rust) aceita:

Valor Exemplo Descrição
Um asset Rust asset!("logo.png"), TSX import logo from "./logo.png" Um ImageAsset { url, size, scales? }, dimensionado em pontos: um arquivo @2x conta metade de seus pixels. Com logo@2x.png / logo@3x.png ao lado de logo.png, a variante correspondente à densidade de pixels da tela é carregada (densidades de pixels). É possível importar .png, .jpg, .jpeg, .gif, .webp e .heic, além de .svg, dimensionado por seus width e height ou viewBox (imagens SVG).
Uma URL "https://example.com/a.jpg" Baixada e armazenada em cache. Downloads da mesma URL são compartilhados entre views.
Um caminho de asset "asset:/images/a.png" Um arquivo dos assets do app.
Um URI data: "data:image/png;base64,iVBOR…" Bytes no próprio URI, em base64 ou com codificação percentual (data:image/svg+xml,%3Csvg…).
Uma URL blob: URL.createObjectURL(blob) Os bytes de um Blob ou File (TSX). A imagem os mantém: revogar a URL deixa uma imagem que continua sendo exibida por inteiro.

Uma imagem criada a partir de bytes que o app já possui é exibida sem requisição. Um chat mostra uma foto escolhida a partir do blob enquanto ela é enviada e, depois, troca src para a URL da 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))

Bytes iguais formam uma única imagem: a mesma imagem como URI data: e como blob é decodificada uma só vez e compartilha o cache.

Durante o desenvolvimento, os assets são servidos pelo servidor de desenvolvimento; nsh build e nsh package os copiam para dentro do app (um crate Rust distribui seu diretório assets/ com registry.assets(assets!())). Veja imagens e assets.

#Props

Além das props comuns (com style tipado como ImageStyle):

Prop Rust Tipo Padrão Descrição
src image(src) string | ImageAsset A imagem.
placeholder .placeholder(src) string | ImageAsset Exibido até que src seja carregado; o ideal é um asset local.
blurRadius .blur_radius(px(..)) number Desfoca a imagem com este raio em pontos (aplicado uma única vez durante a decodificação, fora da thread principal).
fadeDuration .fade_duration(ms) number iOS 0, Android 300 Por quanto tempo uma imagem carregada aparece em fade-in, em milissegundos.
tintColor (estilo) .tint_color(..) ColorValue Recolore uma imagem template (um ícone desenhado em uma só cor).
objectFit (estilo) .object_fit(ResizeMode::Contain) "cover" | "contain" | "fill" | "none" | "scale-down" "cover" Como a imagem se ajusta à sua caixa, como o object-fit do CSS (classes object-*): "cover" a preenche e corta, "contain" a encaixa dentro dela, "fill" a estica, "none" a exibe em seu tamanho natural, "scale-down" o mesmo, reduzida para caber quando é maior.
objectPosition (estilo) .object_position(..), .object_top(), … string "center" Onde a imagem fica em sua caixa depois que objectFit a dimensionou, como o object-position do CSS ("top", "25% 75%", "right 10px bottom 20%"); uma imagem que cobre a caixa é cortada ao redor dessa posição.
onLoadStart .on_load_start(..) () => void O carregamento começou.
onProgress .on_progress(..) (e: { loaded: number; total: number }) => void Progresso do download em bytes (total é 0 quando desconhecido), no máximo a cada 100 ms.
onLoad .on_load(..) (e: { size: PixelSize }) => void Carregada; o tamanho da imagem de origem em pixels (não o da imagem decodificada e reduzida; o tamanho de um SVG em pontos, na web conforme medido pelo navegador); para uma imagem com variantes, o da variante carregada.
onError .on_error(..) (e: { message: string; code?: string }) => void O carregamento falhou; code é "ERR_ACCESS_DENIED" para uma URL que as capabilities do app não permitem.
onLoadEnd .on_load_end(..) () => void Após onLoad ou onError.

ImageStyle adiciona tintColor aos estilos da view, que recolore uma imagem template (um ícone desenhado em uma só cor), e objectPosition, que posiciona a imagem em sua caixa (posição da imagem).

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

#Eventos

Evento Rust Payload Quando
onLoadStart .on_load_start nenhum O carregamento começou.
onProgress .on_progress { loaded, total } (ImageProgress) Chegaram bytes (no máximo a cada 100 ms).
onLoad .on_load { size } (Size<DevicePixels>) A imagem é exibida; o tamanho da origem em pixels (o de um SVG em pontos, na web conforme medido pelo navegador).
onError .on_error { message, code? } (&str, a mensagem) O carregamento ou a decodificação falhou, ou o host está fora das capabilities do app (code "ERR_ACCESS_DENIED").
onLoadEnd .on_load_end nenhum Após onLoad ou 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()))

Uma imagem com variantes de densidade de pixels carrega um de seus arquivos, e onLoad informa os pixels desse arquivo: 192×192 para logo@3x.png em uma tela 3×, para um logo.png de 64×64 pontos. Quando a densidade de pixels muda e a imagem carrega outra variante, ela também informa esse carregamento: onLoadStart, depois onLoad com o tamanho do novo arquivo (ou onError), depois onLoadEnd.

Para saber o tamanho de uma imagem ou carregá-la antes de exibi-la, TSX chama Image.getSize(src) e Image.prefetch(src) (o mesmo Image que renderiza o elemento os expõe; veja a API Image), e Rust usa image_size(src) e prefetch_image(src), com await em uma task. Dada uma imagem importada com variantes, Image.getSize e Image.prefetch carregam a variante que esta tela exibe, e getSize retorna o tamanho do arquivo importado (64×64 para logo.png).

#Métodos de ref

Nenhum próprio.

#Notas de plataforma

  • A chave de cache inclui o tamanho de decodificação e o raio de desfoque: a mesma URL exibida em dois tamanhos é decodificada duas vezes, mas baixada uma só vez.
  • O iOS reproduz imagens animadas como uma animação de quadros-chave (até 600 quadros); uma imagem animada com tint mostra apenas o primeiro quadro.
  • O Android reproduz imagens animadas a partir da API 28 (AnimatedImageDrawable), sem cache e sem desfoque.
  • No Android, as bordas são desenhadas acima da imagem, como no iOS.