Один из наиболее популярных способов сделать элемент 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