Elementi
Image
Image mostra un’immagine dagli asset dell’app o da un URL. Le immagini vengono scaricate e decodificate fuori dal main thread, ridimensionate alla dimensione con cui la vista le mostra e messe in cache in memoria. Le immagini animate GIF, WebP e APNG vengono riprodotte. In Rust se ne crea una con image(src), in TSX con <Image> da nsh.
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(),
)import { Image, View } from "nsh";
import logo from "./assets/logo.png";
<View style={{ flexDirection: "row", gap: 12 }}>
<Image src={logo} />
<Image
src="https://example.com/avatar.jpg"
placeholder={logo}
style={{ width: 64, height: 64, borderRadius: 32 }}
/>
</View>;Un asset conosce le proprie dimensioni: senza larghezza o altezza l’immagine usa quelle dell’asset, e con una sola delle due l’altra segue il rapporto d’aspetto dell’asset. La dimensione di un URL è nota solo dopo il caricamento, quindi per le immagini remote specifica sia la larghezza sia l’altezza (oppure un rapporto d’aspetto insieme a una delle due).
#Sorgenti
La sorgente (src e placeholder; image(..) e .placeholder(..) in Rust) accetta:
| Valore | Esempio | Descrizione |
|---|---|---|
| Un asset | Rust asset!("logo.png"), TSX import logo from "./logo.png" | Un ImageAsset { url, size, scales? }, dimensionato in punti: un file @2x conta la metà dei suoi pixel. Con logo@2x.png / logo@3x.png accanto a logo.png, viene caricata la variante per il rapporto di pixel dello schermo (densità di pixel). Si possono importare .png, .jpg, .jpeg, .gif, .webp e .heic, e .svg, dimensionato dai suoi width e height o viewBox (immagini SVG). |
| Un URL | "https://example.com/a.jpg" | Scaricato e messo in cache. I download dello stesso URL sono condivisi tra le viste. |
| Un percorso di asset | "asset:/images/a.png" | Un file degli asset dell’app. |
Un URI data: | "data:image/png;base64,iVBOR…" | Byte contenuti nell’URI, in base64 o con percent-encoding (data:image/svg+xml,%3Csvg…). |
Un URL blob: | URL.createObjectURL(blob) | I byte di un Blob o File (TSX). L’immagine li conserva: revocare l’URL lascia un’immagine che si mostra per intero. |
Un’immagine ottenuta da byte che l’app possiede già viene mostrata senza alcuna richiesta. Una chat mostra una foto scelta dal suo blob mentre viene caricata, poi passa con src all’URL della 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))import { Image, signal } from "nsh";
const preview = URL.createObjectURL(photo); // the picked `File`
const src = signal(preview);
upload(photo).then((url) => {
src.set(url);
URL.revokeObjectURL(preview);
});
<Image src={src()} style={{ width: 240, height: 180 }} />;Byte uguali sono un’unica immagine: la stessa immagine come URI data: e come blob viene decodificata una sola volta e condivide la cache.
Durante lo sviluppo gli asset sono serviti dal dev server; nsh build e nsh package li copiano nell’app (un crate Rust distribuisce la propria directory assets/ con registry.assets(assets!())). Vedi immagini e asset.
#Props
Oltre alle prop comuni (con style di tipo ImageStyle):
| Prop | Rust | Tipo | Predefinito | Descrizione |
|---|---|---|---|---|
src | image(src) | string | ImageAsset | L’immagine. | |
placeholder | .placeholder(src) | string | ImageAsset | Mostrata finché src non è stata caricata; meglio un asset locale. | |
blurRadius | .blur_radius(px(..)) | number | Sfoca l’immagine di questo raggio in punti (applicato una sola volta durante la decodifica, fuori dal main thread). | |
fadeDuration | .fade_duration(ms) | number | iOS 0, Android 300 | Per quanto tempo un’immagine caricata appare in dissolvenza, in millisecondi. |
tintColor (stile) | .tint_color(..) | ColorValue | Ricolora un’immagine template (un’icona disegnata in un solo colore). | |
objectFit (stile) | .object_fit(ResizeMode::Contain) | "cover" | "contain" | "fill" | "none" | "scale-down" | "cover" | Come l’immagine si adatta al suo riquadro, come object-fit di CSS (classi object-*): "cover" lo riempie e ritaglia, "contain" la fa stare al suo interno, "fill" la deforma per riempirlo, "none" la mostra alla sua dimensione naturale, "scale-down" lo stesso, ma ridotta per adattarsi dove è più grande. |
objectPosition (stile) | .object_position(..), .object_top(), … | string | "center" | Dove si colloca l’immagine nel suo riquadro una volta che objectFit l’ha dimensionata, come object-position di CSS ("top", "25% 75%", "right 10px bottom 20%"); un’immagine che copre viene ritagliata attorno ad essa. |
onLoadStart | .on_load_start(..) | () => void | Il caricamento è iniziato. | |
onProgress | .on_progress(..) | (e: { loaded: number; total: number }) => void | Avanzamento del download in byte (total è 0 se sconosciuto), al massimo ogni 100 ms. | |
onLoad | .on_load(..) | (e: { size: PixelSize }) => void | Caricata; la dimensione dell’immagine sorgente in pixel (non quella decodificata e ridimensionata; per un SVG la dimensione in punti, sul web come la misura il browser); per un’immagine con varianti, quella della variante caricata. | |
onError | .on_error(..) | (e: { message: string; code?: string }) => void | Il caricamento è fallito; code è "ERR_ACCESS_DENIED" per un URL che le capabilities dell’app non concedono. | |
onLoadEnd | .on_load_end(..) | () => void | Dopo onLoad o onError. |
ImageStyle aggiunge agli stili della vista tintColor, che ricolora un’immagine template (un’icona disegnata in un solo colore), e objectPosition, che colloca l’immagine nel suo riquadro (Posizione dell’immagine).
image(ICON).size(px(24.0)).tint_color(rgb(0x0969da))<Image src={icon} style={{ width: 24, height: 24, tintColor: "#0969da" }} />#Eventi
| Evento | Rust | Payload | Quando |
|---|---|---|---|
onLoadStart | .on_load_start | nessuno | Il caricamento è iniziato. |
onProgress | .on_progress | { loaded, total } (ImageProgress) | Sono arrivati dei byte (al massimo ogni 100 ms). |
onLoad | .on_load | { size } (Size<DevicePixels>) | L’immagine è mostrata; la dimensione della sorgente in pixel (per un SVG in punti, sul web come la misura il browser). |
onError | .on_error | { message, code? } (&str, il messaggio) | Il caricamento o la decodifica sono falliti, oppure l’host è fuori dalle capabilities dell’app (code "ERR_ACCESS_DENIED"). |
onLoadEnd | .on_load_end | nessuno | Dopo 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()))const state = signal("loading");
<Image
src={url}
style={{ width: "100%", aspectRatio: 16 / 9 }}
onLoad={({ size }) => state.set(`${size.width}×${size.height} px`)}
onError={(e) => state.set(e.message)}
/>;Un’immagine con varianti di densità di pixel carica uno dei suoi file, e onLoad riporta i pixel di quel file: 192×192 per logo@3x.png su uno schermo 3×, per un logo.png di 64×64 punti. Quando il rapporto di pixel cambia e l’immagine carica un’altra variante, riporta anche quel caricamento: onLoadStart, poi onLoad con la dimensione del nuovo file (oppure onError), poi onLoadEnd.
Per conoscere la dimensione di un’immagine o caricarla prima di mostrarla, in TSX si chiamano Image.getSize(src) e Image.prefetch(src) (lo stesso Image che renderizza l’elemento li espone; vedi l’Image API), mentre in Rust si usano image_size(src) e prefetch_image(src), con await in un task. Data un’immagine importata con varianti, Image.getSize e Image.prefetch caricano la variante che questo schermo mostra, e getSize restituisce la dimensione del file importato (64×64 per logo.png).
#Metodi ref
Nessuno specifico.
#Note sulle piattaforme
- La chiave della cache include la dimensione di decodifica e il raggio di sfocatura: lo stesso URL mostrato a due dimensioni viene decodificato due volte ma scaricato una sola.
- iOS riproduce le immagini animate come animazione a keyframe (fino a 600 frame); un’immagine animata colorata mostra il suo primo frame.
- Android riproduce le immagini animate a partire da API 28 (
AnimatedImageDrawable), senza cache e senza sfocatura. - Su Android i bordi sono disegnati sopra l’immagine, come su iOS.