Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active June 25, 2025 00:35
Show Gist options
  • Save PlugFox/87948692684838afc9efd839e83d0903 to your computer and use it in GitHub Desktop.
Save PlugFox/87948692684838afc9efd839e83d0903 to your computer and use it in GitHub Desktop.
Flutter Shimmer 2025
project
└───packages
    └───ui
        ├───lib
        │   ├───shaders
        │   │   └─── shimmer.frag
        │   ├───src
        │   │   └─── shimmer.dart
        │   └───ui.dart
        └─── pubspec.yaml
name: ui
resolution: workspace
environment:
sdk: ">=3.7.0 <4.0.0"
flutter: ">=3.29.3"
dependencies:
flutter:
sdk: flutter
flutter:
shaders:
- packages/ui/shaders/shimmer.frag
import 'dart:developer' as developer;
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
/// {@template shimmer}
/// A widget that creates a shimmering effect similar
/// to a moving highlight or reflection.
/// This is commonly used as a placeholder or loading indicator.
/// {@endtemplate}
/// {@category shaders}
class Shimmer extends LeafRenderObjectWidget {
/// Creates a shimmer effect with the specified [highlight], [speed], [size], and [radius].
/// {@macro stepper}
const Shimmer({
this.highlight = const ui.Color(0xFFEEEEEE),
this.background = const Color(0xFFFFFFFF),
this.speed = 1.0,
this.size,
this.radius,
this.stripe,
super.key,
});
/// The color of the shimmer effect.
/// Defaults to a light color, slightly off-white.
/// This color is used to create the shimmering highlight.
final Color highlight;
/// The background color of the shimmer effect.
/// Better to use a background color of the parent widget.
final Color background;
/// The speed of the shimmer effect, where 1.0 is the default speed.
final double speed;
/// The size of the shimmer effect.
/// If null, it will use the size of the parent widget.
final Size? size;
/// The radius for rounded corners of the shimmer effect.
/// If null, it will not apply any rounded corners.
final Radius? radius;
/// Size of the stripe in the shimmer effect.
/// One of the best choice is between 0.5 .. 1.0
final double? stripe;
@override
RenderObject createRenderObject(BuildContext context) => ShimmerRenderObject(
highlight: highlight, // Shimmer (primary) highlight color for the shimmer effect
background: background, // Background (secondary) color for the shimmer effect
speed: speed, // Speed of the shimmer effect multiplier
size: size, // Size of the shimmer effect
radius: radius, // Radius for rounded corners
stripe: stripe, // Size of the stripe in the shimmer effect
);
@override
void updateRenderObject(BuildContext context, covariant ShimmerRenderObject renderObject) => renderObject.update(
highlight: highlight, // Update shimmer (primary) highlight color for the shimmer effect
background: background, // Update background (secondary) color for the shimmer effect
speed: speed, // Speed of the shimmer effect multiplier
size: size, // Size of the shimmer effect
radius: radius, // Radius for rounded corners
stripe: stripe, // Size of the stripe in the shimmer effect
);
}
class ShimmerRenderObject extends RenderBox with WidgetsBindingObserver {
ShimmerRenderObject({
required Color highlight,
required Color background,
required double speed,
required Size? size,
required Radius? radius,
required double? stripe,
}) : _highlight = highlight,
_background = background,
_speed = speed,
_size = size,
_radius = radius,
_stripe = stripe,
_paint = Paint() {
_paint
..color = background
..style = PaintingStyle.fill
..blendMode = BlendMode.srcOver
..filterQuality = FilterQuality.low
..isAntiAlias = true;
}
Color _highlight;
Color _background;
double _speed;
Size? _size;
Radius? _radius;
double? _stripe;
final Paint _paint;
/// Animation vsync ticker for the shimmer effect.
Ticker? _animationTicker;
void update({
required Color highlight,
required Color background,
required double speed,
required Size? size,
required Radius? radius,
required double? stripe,
}) {
if (size != _size) {
markNeedsLayout();
}
_highlight = highlight;
_background = background;
_speed = speed;
_size = size;
_radius = radius;
_stripe = stripe;
_paint.color = background;
markNeedsPaint();
}
Size _$size = Size.zero;
@override
Size get size => _$size;
@override
set size(Size value) {
final prev = super.hasSize ? super.size : null;
super.size = value;
if (prev == value) return;
_$size = value;
}
int _activeFlag = 0;
@override
bool get isRepaintBoundary => false;
@override
bool get alwaysNeedsCompositing => false;
@override
bool get sizedByParent => false;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
const lifecycleFlag = 1 << 0; // Flag to indicate lifecycle changes
if (state == AppLifecycleState.resumed) {
_activeFlag &= ~lifecycleFlag; // Clear the active flag when the app is resumed
} else {
_activeFlag |= lifecycleFlag; // Set the active flag for other states
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_activeFlag &= ~(1 << 1); // Clear the active flag when attached
WidgetsBinding.instance.addObserver(this);
// Load the shader if it hasn't been loaded yet
_ShimmerShaderManager.setShader(_paint);
_animationTicker = Ticker(_onTick)..start();
}
Duration _elapsed = Duration.zero;
void _onTick(Duration elapsed) {
_elapsed = elapsed;
if (_activeFlag != 0) return; // Only update if the active flag is set
// Perform the shimmer effect update logic here
markNeedsPaint();
}
@override
@protected
void detach() {
super.detach();
_activeFlag |= (1 << 1); // Set the active flag when detached
_animationTicker?.dispose();
WidgetsBinding.instance.removeObserver(this);
}
@override
bool hitTestSelf(Offset position) => false;
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) => false;
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) => false;
@override
Size computeDryLayout(BoxConstraints constraints) => switch (_size) {
Size size => constraints.constrain(size),
_ => constraints.biggest,
};
@override
void performLayout() {
size = computeDryLayout(constraints);
}
@override
void performResize() {
size = computeDryLayout(constraints);
}
@override
void paint(PaintingContext context, Offset offset) {
final size = this.size;
if (size.isEmpty) return; // No need to paint if size is empty
final canvas =
context.canvas
..save()
..translate(offset.dx, offset.dy);
// Clip the canvas to the size and radius if provided
if (_radius case Radius radius when radius != Radius.zero) {
canvas.clipRRect(RRect.fromRectAndRadius(Offset.zero & size, _radius ?? Radius.zero));
} else {
canvas.clipRect(Offset.zero & size);
}
if (_paint.shader case ui.FragmentShader shader) {
// If the shader is available, apply it to the paint
final seed = _elapsed.inMicroseconds * _speed / 200000;
_paint.shader =
shader
..setFloat(0, size.width)
..setFloat(1, size.height)
..setFloat(2, seed)
..setFloat(3, _highlight.r)
..setFloat(4, _highlight.g)
..setFloat(5, _highlight.b)
..setFloat(6, _highlight.a)
..setFloat(7, _background.r)
..setFloat(8, _background.g)
..setFloat(9, _background.b)
..setFloat(10, _background.a)
..setFloat(11, _stripe ?? 0.75);
canvas.drawRect(Offset.zero & size, _paint);
} else {
// If the shader is not available, draw a solid color
canvas.drawRect(Offset.zero & size, _paint);
}
canvas.restore();
}
}
abstract final class _ShimmerShaderManager {
static ui.FragmentProgram? _$fragmentProgram;
static final Future<ui.FragmentProgram?> _$loadfragmentProgramOnce = _$loadfragmentProgram();
static Future<ui.FragmentProgram?> _$loadfragmentProgram() async {
const asset = 'packages/ui/shaders/shimmer.frag';
try {
final program = _$fragmentProgram = await ui.FragmentProgram.fromAsset(asset).timeout(const Duration(seconds: 5));
return program;
} on UnsupportedError {
return null; // Thats fine for HTML Renderer and unsupported platforms.
} catch (e, s) {
developer.log('Failed to load shader: $e', error: e, stackTrace: s, name: 'ui', level: 700);
FlutterError.reportError(
FlutterErrorDetails(
exception: e,
stack: s,
library: 'ui',
context: ErrorDescription('Failed to load shimmer shader'),
),
);
return null;
}
}
/// The shader to be used for the shimmer effect to be applied to the paint.
static void setShader(Paint paint) {
if (_$fragmentProgram case ui.FragmentProgram program) {
paint
..shader = program.fragmentShader()
..blendMode = BlendMode.src
..filterQuality = FilterQuality.low
..isAntiAlias = false;
} else {
_$loadfragmentProgramOnce.then((program) {
if (program == null) return;
paint
..shader = program.fragmentShader()
..blendMode = BlendMode.src
..filterQuality = FilterQuality.low
..isAntiAlias = false;
}).ignore();
}
}
}
#version 460 core
#define SHOW_GRID
#include <flutter/runtime_effect.glsl>
uniform vec2 u_size; // size of the shape
uniform float u_seed; // shader playback time (in seconds)
uniform vec4 u_color_highlight; // line color of the shape
uniform vec4 u_color_background; // background color of the shape
uniform float u_stripe; // width of the stripes
out vec4 fragColor;
void main() {
// Direction vector for 30 degrees angle (values are precalculated)
vec2 direction = vec2(0.866, 0.5);
// Calculate normalized coordinates
vec2 normalizedCoords = gl_FragCoord.xy / u_size;
// Generate a smooth moving wave based on time and coordinates
float waveRaw = 0.5 * (1.0 + sin(u_seed - dot(normalizedCoords, direction) * u_stripe * 3.1415));
float wave = smoothstep(0.0, 1.0, waveRaw);
// Use the wave to interpolate between the background color and line color
vec4 color = mix(u_color_background, u_color_highlight, wave);
fragColor = color;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment