Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Image

Image показывает изображение из ресурсов приложения или по URL. Изображения загружаются и декодируются вне главного потока, уменьшаются до размера, в котором их показывает представление, и кэшируются в памяти. Анимированные GIF, WebP и APNG воспроизводятся. В Rust его создаёт image(src), в TSX — <Image> из 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(),
    )

Размер ресурса известен заранее: если не заданы ни ширина, ни высота, изображение берёт его, а если задана одна из сторон, вторая вычисляется по соотношению сторон ресурса. Размер URL становится известен только после загрузки, поэтому удалённым изображениям задавайте и ширину, и высоту (или соотношение сторон и одну из них).

#Источники

Источник (src и placeholder; image(..) и .placeholder(..) в Rust) принимает:

ЗначениеПримерОписание
РесурсRust asset!("logo.png"), TSX import logo from "./logo.png"ImageAsset { url, size, scales? } с размером в пунктах: файл @2x считается как половина его пикселей. Если logo@2x.png / logo@3x.png лежат рядом с logo.png, загружается вариант для плотности пикселей экрана (плотности пикселей). Можно импортировать .png, .jpg, .jpeg, .gif, .webp и .heic, а также .svg с размером по width и height или viewBox (SVG-изображения).
URL"https://example.com/a.jpg"Загружается и кэшируется. Загрузки одного и того же URL разделяются между представлениями.
Путь к ресурсу"asset:/images/a.png"Файл из ресурсов приложения.
URI data:"data:image/png;base64,iVBOR…"Байты в самом URI в base64 или с процентным кодированием (data:image/svg+xml,%3Csvg…).
URL blob:URL.createObjectURL(blob)Байты Blob или File (TSX). Изображение сохраняет их: после отзыва URL оно по-прежнему показывается целиком.

Изображение из байтов, которые уже есть у приложения, показывается без запроса. Чат показывает выбранное фото из его 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))

Одинаковые байты — это одно изображение: одна и та же картинка в виде URI data: и в виде blob декодируется один раз и использует общий кэш.

Во время разработки ресурсы раздаёт dev-сервер; nsh build и nsh package копируют их в приложение (крейт Rust поставляет свой каталог assets/ вместе с registry.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 — в пунктах, в вебе так, как его измеряет браузер); для изображения с вариантами — размер загруженного варианта.
onError.on_error(..)(e: { message: string; code?: string }) => voidЗагрузка не удалась; code равен "ERR_ACCESS_DENIED" для URL, не разрешённого capabilities приложения.
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 — в пунктах, в вебе так, как его измеряет браузер).
onError.on_error{ message, code? } (&str, сообщение)Загрузка или декодирование не удались, либо хост не входит в capabilities приложения (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 сообщает размер этого файла в пикселях: 192×192 для logo@3x.png на экране 3× при logo.png размером 64×64 пункта. Когда плотность пикселей меняется и изображение загружает другой вариант, оно сообщает и об этой загрузке: onLoadStart, затем onLoad с размером нового файла (или onError), затем onLoadEnd.

Чтобы узнать размер изображения или загрузить его до показа, в TSX вызывают Image.getSize(src) и Image.prefetch(src) (их несёт тот же Image, что отрисовывает элемент; см. Image API), а в Rust — image_size(src) и prefetch_image(src), с ожиданием в задаче. Для импортированного изображения с вариантами Image.getSize и Image.prefetch загружают вариант, который показывает этот экран, а getSize возвращает размер импортированного файла (64×64 для logo.png).

#Методы ref

Собственных методов нет.

#Особенности платформ

  • Ключ кэша включает размер декодирования и радиус размытия: один и тот же URL, показанный в двух размерах, декодируется дважды, а загружается один раз.
  • iOS воспроизводит анимированные изображения как покадровую анимацию (до 600 кадров); анимированное изображение с наложенным цветом показывает только первый кадр.
  • Android воспроизводит анимированные изображения начиная с API 28 (AnimatedImageDrawable), без кэширования и без размытия.
  • На Android границы рисуются поверх изображения, как и на iOS.