Skip to content

Instantly share code, notes, and snippets.

@hawkkiller
Created September 9, 2025 14:30
Show Gist options
  • Select an option

  • Save hawkkiller/2ed40b43d6372fc322080e0c8ce76521 to your computer and use it in GitHub Desktop.

Select an option

Save hawkkiller/2ed40b43d6372fc322080e0c8ce76521 to your computer and use it in GitHub Desktop.
Shimmer implemented with linear gradient and a shimmer implemented with custom shader.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Shimmer extends StatefulWidget {
const Shimmer({super.key, this.size, this.child});
/// The size of the shimmer effect.
///
/// Either this or [child] must be provided.
/// If both are provided, [child] takes precedence.
final Size? size;
/// The child widget to apply the shimmer effect to.
///
/// Either this or [size] must be provided.
/// If both are provided, [child] takes precedence.
final Widget? child;
@override
State<Shimmer> createState() => _ShimmerState();
}
class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
late final _controller = AnimationController(vsync: this);
late final _tween = Tween<double>(
begin: -1,
end: 1.5,
).animate(CurvedAnimation(parent: _controller, curve: Curves.linear));
@override
void initState() {
super.initState();
_controller
..duration = const Duration(milliseconds: 2000)
..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
size: widget.size ?? Size.zero,
painter: _ShimmerPainter(
animation: _tween,
stops: [0, .5, 1],
backgroundColor: Colors.black.withValues(alpha: .16),
colors: [
Colors.black.withValues(alpha: 0),
Colors.black.withValues(alpha: .16),
Colors.black.withValues(alpha: 0),
],
),
child: Visibility.maintain(
visible: false,
child: widget.child ?? SizedBox.shrink(),
),
);
}
}
class _ShimmerPainter extends CustomPainter {
const _ShimmerPainter({
required this.animation,
required this.colors,
required this.stops,
this.backgroundColor,
}) : super(repaint: animation);
final Animation<double> animation;
final Color? backgroundColor;
final List<Color> colors;
final List<double> stops;
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
// Draw background color first if provided
if (backgroundColor != null) {
final backgroundPaint = Paint()..color = backgroundColor!;
canvas.drawRect(rect, backgroundPaint);
}
final paint = Paint();
paint.shader = LinearGradient(
colors: colors,
stops: stops,
begin: Alignment.centerLeft,
end: Alignment.centerRight,
tileMode: TileMode.clamp,
transform: _SlidingGradientTransform(animation.value),
).createShader(rect);
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant _ShimmerPainter oldDelegate) =>
oldDelegate.animation != animation ||
listEquals(oldDelegate.colors, colors) ||
listEquals(oldDelegate.stops, stops) ||
oldDelegate.backgroundColor != backgroundColor;
}
class _SlidingGradientTransform extends GradientTransform {
const _SlidingGradientTransform(this.offset);
final double offset;
@override
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
final resolvedOffset =
textDirection == TextDirection.rtl ? -offset : offset;
return Matrix4.translationValues(bounds.width * resolvedOffset, 0.0, 0.0);
}
}
#version 460 core
#include <flutter/runtime_effect.glsl>
// Uniforms (variables passed from your Flutter code)
uniform float u_offset; // The animation value to slide the gradient
uniform vec2 u_resolution; // The width and height of the paint area
uniform vec4 u_colors[3]; // The 3 colors of the gradient
uniform float u_stops[3]; // The 3 color stops for the gradient
out vec4 fragColor;
void main() {
// Get the normalized horizontal coordinate (0.0 to 1.0)
float x = FlutterFragCoord().x / u_resolution.x;
// Apply the sliding offset to the coordinate
float pos = x - u_offset;
vec4 color;
// Check if the current pixel is outside the gradient's defined range
if (pos < u_stops[0] || pos > u_stops[2]) {
// If outside, make it fully transparent
color = vec4(0.0, 0.0, 0.0, 0.0);
}
// Check if the pixel is in the first half of the gradient
else if (pos < u_stops[1]) {
// Calculate the interpolation factor 't' for the first segment
float t = (pos - u_stops[0]) / (u_stops[1] - u_stops[0]);
// Linearly interpolate (mix) between the first and second color
color = mix(u_colors[0], u_colors[1], t);
}
// The pixel must be in the second half of the gradient
else {
// Calculate the interpolation factor 't' for the second segment
float t = (pos - u_stops[1]) / (u_stops[2] - u_stops[1]);
// Linearly interpolate (mix) between the second and third color
color = mix(u_colors[1], u_colors[2], t);
}
// Output the final calculated color for the pixel
fragColor = color;
}
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class ShimmerFrag extends StatefulWidget {
const ShimmerFrag({super.key, this.size, this.child});
final Size? size;
final Widget? child;
@override
State<ShimmerFrag> createState() => _ShimmerFragState();
}
class _ShimmerFragState extends State<ShimmerFrag> with SingleTickerProviderStateMixin {
late final _controller = AnimationController(vsync: this);
late final _tween = Tween<double>(
begin: -1,
end: 1.5,
).animate(CurvedAnimation(parent: _controller, curve: Curves.linear));
// A future to hold the loaded shader program
late final Future<ui.FragmentProgram> _shaderFuture;
// Colors and stops are defined here for clarity
final _backgroundColor = Colors.black.withAlpha(41); // ~16% opacity
final _colors = [
Colors.black.withAlpha(0),
Colors.black.withAlpha(41),
Colors.black.withAlpha(0),
];
final _stops = [0.0, 0.5, 1.0];
@override
void initState() {
super.initState();
_shaderFuture = _loadShader();
_controller
..duration = const Duration(milliseconds: 2000)
..repeat();
}
// Helper to load the shader program from assets
Future<ui.FragmentProgram> _loadShader() {
return ui.FragmentProgram.fromAsset('shaders/shimmer.frag');
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<ui.FragmentProgram>(
future: _shaderFuture,
builder: (context, snapshot) {
// Show a placeholder while the shader is loading
if (!snapshot.hasData) {
return Container(
width: widget.size?.width,
height: widget.size?.height,
color: _backgroundColor,
child: Visibility.maintain(
visible: false,
child: widget.child ?? const SizedBox.shrink(),
),
);
}
// Once loaded, use the CustomPaint with the shader painter
return CustomPaint(
size: widget.size ?? Size.zero,
painter: _ShimmerShaderPainter(
program: snapshot.data!,
animation: _tween,
stops: _stops,
backgroundColor: _backgroundColor,
colors: _colors,
),
child: Visibility.maintain(
visible: false,
child: widget.child ?? const SizedBox.shrink(),
),
);
},
);
}
}
class _ShimmerShaderPainter extends CustomPainter {
const _ShimmerShaderPainter({
required this.program,
required this.animation,
required this.colors,
required this.stops,
this.backgroundColor,
}) : super(repaint: animation);
final ui.FragmentProgram program;
final Animation<double> animation;
final Color? backgroundColor;
final List<Color> colors;
final List<double> stops;
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
// 1. Draw the background color first
if (backgroundColor != null) {
final backgroundPaint = Paint()..color = backgroundColor!;
canvas.drawRect(rect, backgroundPaint);
}
// 2. Prepare uniforms for the shader
final floatUniforms = [
// u_offset
animation.value,
// u_resolution
size.width,
size.height,
// u_colors (flattened into a list of RGBA components)
...colors.expand(
(c) => [c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255],
),
// u_stops
...stops,
];
// 3. Create the shader from the program and set its uniforms
final shader = program.fragmentShader();
for (var i = 0; i < floatUniforms.length; i++) {
shader.setFloat(i, floatUniforms[i]);
}
final paint = Paint()..shader = shader;
// 4. Draw the shimmer effect over the background
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant _ShimmerShaderPainter oldDelegate) =>
oldDelegate.program != program ||
oldDelegate.animation.value != animation.value ||
!listEquals(oldDelegate.colors, colors) ||
!listEquals(oldDelegate.stops, stops) ||
oldDelegate.backgroundColor != backgroundColor;
}
@raslan1994
Copy link

Some suggestions: When you are using the Shimmer widget try wrapping it with a RepaintBoundary widget followed buy a ValueKey, would give more performance and reduce the renders.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment