Один из наиболее популярных способов сделать элемент UI выразительным и аутентичным состоит в том, чтобы заполнить его картинкой, градиентом или анимированной гифкой.
Ниже приведен пример реализации данных эффектов на Flutter. Представленный подход будет работать с любым виджетом на всех поддерживаемых платформах. В качестве примера мы будем заполнять графикой Text.
План
Реализация будет идти по сценарию, предложенному командой разработчиков Flutter в серии видео "Widget of the Week".
Основные шаги для заполнения текста графикой:
Создать TextChild виджет для отображения текста.
Создать Shader с нашей кастомной графикой.
Применить 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.
Заключение
Благодарю за чтение!
Не стесняйтесь писать внизу свои мысли, предложения и пожелания по поводу данной статьи. Я буду рад учесть их в своих следующих публикациях, а также отвечу на любые вопросы в комментариях.
Ставьте лайки, если статья оказалась полезна.
Об авторе