Как стать автором
Обновить

Заполнить виджет градиентом, изображением или гифкой с помощью ShaderMask

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров2.7K

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

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии4

Публикации

Истории

Работа

iOS разработчик
17 вакансий
Веб дизайнер
25 вакансий
Swift разработчик
19 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань