Hola, Amigos! Меня зовут Сергей Климович, я Mobile Team Lead агентства заказной разработки Amiga. В мире мобильной разработки Flutter выделяется своей гибкостью и простотой в создании красивых пользовательских интерфейсов. Однако, чтобы добавить дополнительные визуальные эффекты и повысить уровень графической привлекательности приложения, иногда необходимо выходить за рамки стандартных возможностей. И здесь на сцену выходят шейдеры.
Шейдеры предоставляют разработчикам возможность создавать сложные визуальные эффекты, изменять внешний вид элементов интерфейса и даже реализовывать анимации, которых было бы трудно достичь с использованием обычных методов. В этой статье мы рассмотрим, как интегрировать и использовать шейдеры в приложениях Flutter, открыв новые горизонты для креативной реализации дизайнерских идей.

Во Flutter уже встроены несколько шейдеров, которые используются в классах LinearGradient, RadialGradient, SweepGradient, ImageShader и т.д. Данные шейдеры мы можем извлечь из этих объектов и использовать их в таких классах, как ShaderMask или CustomPaint. Но что если нужно создать уникальные градиенты, теневые эффекты или реалистичные анимации.
Можно создать собственный шейдер, сделав несколько манипуляций:
Написать шейдер на языке GLSL и поместить его в проект.
Скомпилировать шейдер внешним компилятором в файл SPIR‑V.
Загрузить файл SPIR‑V во Flutter.
Скомпилировать во Flutter SPIR‑V файл.
Создать шейдер из ранее скомпилированного файла SPIR‑V.
Передать этот шейдер в CustomPaint.
Чтобы создать шейдер, нам нужно написать код на GLSL. GLSL — это язык шейдеров высокого уровня, который используется для программирования графического процессора (GPU). Давайте пропустим часть кодирования GLSL и выберем уже закодированный GLSL и изменим его, чтобы он работал с нашим приложением Flutter.
Чтобы было более предметно и красиво для примера я выберу использование эффекта ниже в шиммере:

Создание шейдера
Создадим файл
shader.fragи скопируем код: https://www.shadertoy.com/view/fd33znЗатем добавим в начало:
#include <flutter/runtime_effect.glsl> // импорт среды выполнения Flutter uniform vec2 uSize; // универсальная переменная, в которой хранится размер визуализируемого объекта uniform float iTime; // универсальная переменная, в которой хранится время, прошедшее с момента запуска шейдера vec2 iResolution; // переменная, в которой хранится разрешение экрана out vec4 fragColor; // выходная переменная, в которой хранится окончательный цвет визуализируемого объекта
Переименуем
mainImage в mainи заменим параметры наvoid.Добавим две переменные внутри:
iResolution = uSize; vec2 fragCoord = FlutterFragCoord();
Интеграция во Flutter
Чтобы не писать много кода для шиммера я просто форкну flutter_shimmer. Создадим отдельный компонент, который будет принимать дочерний элемент и шейдер. Затем мы наложим полученный шейдер поверх него.
class ShimmerFromShader extends StatefulWidget { final Widget child; final FragmentShader shader;
Далее создаем SingleChildRenderObjectWidget для получения объекта рендеринга, принадлежащий дочернему виджету.
Сам рендер будет происходить в _ShimmerFilter, где мы создаем ShaderMaskLayer и на него будем накладывать наш шейдер. В настройках таймера можно поиграть со скоростью вращения.
class _ShimmerFilter extends RenderProxyBox { FragmentShader shader; double _time; _ShimmerFilter(this.shader, this._time); @override ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?; @override bool get alwaysNeedsCompositing => child != null; set time(double newValue) { if (newValue == _time) { return; } _time = newValue; markNeedsPaint(); } @override void paint(PaintingContext context, Offset offset) { if (child != null) { assert(needsCompositing); layer ??= ShaderMaskLayer(); shader.setFloat(0, size.width); shader.setFloat(1, size.height); shader.setFloat(2, _time); layer! ..shader = shader ..maskRect = Offset.zero & size ..blendMode = BlendMode.srcIn; context.pushLayer(layer!, super.paint, offset); } else { layer = null; } }
Далее сам пример
Добавьте шейдер в Flutter pubspec.yaml:
flutter: shaders: - shaders/shader.frag
Создаем объект шейдера:
Future<FragmentShader> loadMyShader() async { final program = await FragmentProgram.fromAsset('shaders/shader.frag'); shader = program.fragmentShader(); return shader!; }
И далее передаем в созданный ранее виджет, используя в качестве placeholderов виджеты из форкнутого пакета:
ShimmerFromShader.fromShader( shader: snapshot.data!, child: const SingleChildScrollView( physics: NeverScrollableScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ BannerPlaceholder(), TitlePlaceholder(width: double.infinity), SizedBox(height: 16.0), ContentPlaceholder( lineType: ContentLineType.threeLines, ), SizedBox(height: 16.0), TitlePlaceholder(width: 200.0), SizedBox(height: 16.0), ContentPlaceholder( lineType: ContentLineType.twoLines, ), SizedBox(height: 16.0), TitlePlaceholder(width: 200.0), SizedBox(height: 16.0), ContentPlaceholder( lineType: ContentLineType.twoLines, ), ], ), ));
Полный код можно посмотреть тут.
Заключение
В мире Flutter шейдеры предоставляют захватывающие возможности для создания уникальных и впечатляющих пользовательских интерфейсов. Они могут стать мощным инструментом в ваших руках, наполняйте свои проекты индивидуальностью и выразительностью!
Хорошего всем кода!
