Один из наиболее популярных способов сделать элемент UI выразительным и аутентичным состоит в том, чтобы заполнить его картинкой, градиентом или анимированной гифкой.

Ниже приведен пример реализации данных эффектов на Flutter. Представленный подход будет работать с любым виджетом на всех поддерживаемых платформах. В качестве примера мы будем заполнять графикой Text.

План

Реализация будет идти по сценарию, предложенному командой разработчиков Flutter в серии видео "Widget of the Week".

Основные шаги для заполнения текста графикой:

  1. Создать TextChild виджет для отображения текста.

  2. Создать Shader с нашей кастомной графикой.

  3. Применить Shader к TextChild с помощью ShaderMask.

Widget buildBeautifulText() {
  // 1. Create text child
  final textChild = TextChild();
  
  // 2. Create shader
  final shaderCallback = createShader();
  
  // 3. Apply shader to text child
  return ShaderMask(
    blendMode: BlendMode.srcIn,
    shaderCallback: shaderCallback,
    child: textChild,
  );
}

Кейс 1. Шейдер градиента

Заполнить виджет градиентом достаточно легко, и при удачном применении данного эффекта можно получить весьма впечатляющие результаты. Для реализации нам потребуется описать желаемый градиент и попросить его создать шейдер. Полученный шейдер можно сразу применить к дочернему виджету, используя ShaderMask.

// Create gradient shader
ShaderCallback gradientShader() {
  // Define linear gradient
  const gradient = LinearGradient(
    colors: [
      Colors.red,
      Colors.blue,
    ],
    begin: Alignment.topLeft,
    end: Alignment.bottomRight,
    tileMode: TileMode.mirror,
  );
  // Create shader
  final shaderCallback = gradient.createShader;
  return shaderCallback;
}

// Build text with gradient shader
Widget buildBeautifulText() {
  // 1. Create text child
  final textChild = TextChild();

  // 2. Create shader callback
  final shaderCallback = gradientShader();

  // 3. Apply shader to text child
  return ShaderMask(
    blendMode: BlendMode.srcIn,
    shaderCallback: shaderCallback,
    child: textChild,
  );
}

В примере мы использовали линейный градиент от красного к синему. Процесс можно также повторить с градиентами любого типа (Linear, Radial, Sweep).

Полный код для данного кейса можно посмотреть и запустить через DartPad.

Кейс 2. Шейдер изображения

В Flutter есть класс ImageShader, который позволяет создавать шейдеры для изображений. Для этого необходимо передать ему данные изображения.

Шаг 1. Создать ImageProvider

Мы будем использовать ImageProvider, чтобы получить данные изображения из любого доступного источника (сеть, assets, файл, память). ImageProvider поддерживает форматы JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP и WBMP.

// 1. Create ImageProvider
// JPEG
const jpegProvider = NetworkImage(
  'https://picsum.photos/1000',
);

// Animated GIF
const gifProvider = NetworkImage(
  'https://media.giphy.com/media/5VKbvrjxpVJCM/giphy.gif',
);

Шаг 2. Создать ImageShader для изображения

ImageProvider поставляет нам экземпляры класса Image из пакета dart:ui, которые мы можем передать в ImageShader, чтобы создать ShaderCallback. Данный колбэк создает шейдер с учетом границ поверхности, к которой он будет применен.

Мы заполним графикой весь прямоугольник. В этом процессе будем использовать преобразования матрицы Matrix4, чтобы изменить размер и отцентровать изображение.

Можно пойти альтернативным путем и вместо изменений матрицы изменить TileMode. Режимы mirror и repeat должны хорошо себя проявить при работе с повторяющимися узорами.

Код этого шага можно изменить, чтобы настроить сочетания Matrix4 и TileMode для реализации своего уникального дизайна.

// 2. Create image shader for given Rect size
ShaderCallback createImageShader(ui.Image image) {
  shaderCalback(Rect bounds) {
    // Calculate scale for X and Y sides
    final scaleX = bounds.width / image.width;
    final scaleY = bounds.height / image.height;
    final scale = max(scaleX, scaleY);
    // Calculate offset to center resized image
    final scaledImageWidth = image.width * scale;
    final sacledImageHeight = image.height * scale;
    final offset = Offset(
      (scaledImageWidth - bounds.width) / 2,
      (sacledImageHeight - bounds.height) / 2,
    );
    final matrix = Matrix4.identity()
      // Scale image
      ..scale(scale, scale)
      // Center horizontally and vertically
      ..leftTranslate(
        -offset.dx,
        -offset.dy,
      );
    // Image shader
    return ImageShader(
      image,
      TileMode.decal,
      TileMode.decal,
      matrix.storage,
    );
  }

Шаг 3. Подписаться на ImageStream

При использовании ImageProvider у нас есть возможность подписаться на ImageStream, который отслеживает текущий кадр изображения. Подписка на данный поток позволит нам реагировать на изменения изображения, которые происходят в результате анимаций или изменений исходного ресурса изображения.

Документация Flutter уже содержит код использования ImageStream в виджете. Нам остается лишь изменить его метод build и добавить поле child для дочернего виджета.

Добавляем поле child:

// See MyImage class from Flutter docs
// https://api.flutter.dev/flutter/painting/ImageProvider-class.html
class ImageShaderBuilder extends StatefulWidget {
  const ImageShaderBuilder({
    super.key,
    required this.imageProvider,
    // Add child widget
    required this.child,
  });

  // Add child widget
  final Widget child;
  final ImageProvider imageProvider;

  @override
  State<ImageShaderBuilder> createState() => _ImageShaderBuilderState();
}

Изменяем метод build:

// See MyImage class from Flutter docs
// https://api.flutter.dev/flutter/painting/ImageProvider-class.html
class _ImageShaderBuilderState extends State<ImageShaderBuilder> {
  
  // Keep the source code

  // Change only build method
  @override
  Widget build(BuildContext context) {
    final image = _imageInfo?.image;
    // No image for shader -> show child
    if (image == null) {
      return widget.child;
    }
    final shaderCallback = createImageShader(image);
    // Apply shader to the child
    return ShaderMask(
      blendMode: BlendMode.srcIn,
      shaderCallback: shaderCallback,
      child: widget.child,
    );
  }
}

Шаг 4. Использовать шейдер изображения

Теперь мы можем использовать ImageShaderBuilder для реализации ярких и запоминающихся пользовательских интерфейсов.

Widget buildBeautifulText() {
  // 1. Create text child
  const textChild = TextChild();

  // 2. Create ImageProvider
  const imageProvider = NetworkImage(
    'https://media.giphy.com/media/5VKbvrjxpVJCM/giphy.gif',
  );

  // 3. Apply shader to text child
  return const ImageShaderBuilder(
    imageProvider: imageProvider,
    child: textChild,
  );
}

Полный код для данного кейса можно посмотреть и запустить через DartPad.

Заключение

Благодарю за чтение!

Не стесняйтесь писать внизу свои мысли, предложения и пожелания по поводу данной статьи. Я буду рад учесть их в своих следующих публикациях, а также отвечу на любые вопросы в комментариях.

Ставьте лайки, если статья оказалась полезна.

Об авторе

  • Имя: Иван Мосягин (LinkedIn)

  • Компания: Shark Company (LinkedIn)

  • Должность: Flutter Developer

Комментарии (2)


  1. puffleeck
    03.12.2022 18:50
    -1

    эм... написать целую кучу кода и из комментариев к нему склепать статью...

    и всё это, чтобы что? переизобрести пару CSS свойств? уж на ВЭБ фреймворке то? серьёзно?

    #abc{background-clip: text; color: transparent;}

    или может это я чего то не понимаю?...


    1. arthurshark Автор
      03.12.2022 19:04

      Спасибо за комментарий!

      Ваше замечание может быть очень полезно также и для других пользователей, знакомых с веб разработкой. Я постараюсь, не углубляясь в технические детали, ответить достаточно подробно, чтобы не возникло непонимания.

      Вы верно заметили, что при работе с веб-фреймворками эффект, реализуемый в статье, может быть быстро получен через CSS.

      Дело в том, что Flutter – это фреймворк для кроссплатформенной разработки, а не веб фреймворк. В отличие от других популярных кроссплатформенных решений (таких как React Native и Ionic), использующих веб-компоненты для отображения интерфейса, Flutter использует свой собственный набор виджетов, за отображение которых отвечает графический движок Skia (подробнее об этом можно почитать в документации).

      Выбранный разработчиками Flutter способ рендеринга не позволяет использовать CSS для отображения элементов интерфейса, так как, с точки зрения браузера, Flutter на вебе – это Canvas, и для отдельных пикселей на нем не получится применить CSS стиль. Также Flutter поддерживает другие платформы (Android, iOS, Linux, macOS, Windows), на которых данный эффект должен работать без обращения к CSS. Поэтому в статье рассматривается способ реализации эффекта с помощью шейдеров.

      P.S. Я ценю ваше мнение и заботу обо мне и моих читателях, когда вы указываете на проблемные и ошибочные места в статье, но мне было неприятно читать комментарий, оформленный настолько эмоционально и провокационно. Я буду очень признателен, если другие комментарии окажутся более вежливыми.