Created
September 9, 2025 14:30
-
-
Save hawkkiller/2ed40b43d6372fc322080e0c8ce76521 to your computer and use it in GitHub Desktop.
Shimmer implemented with linear gradient and a shimmer implemented with custom shader.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Some suggestions: When you are using the
Shimmerwidget try wrapping it with aRepaintBoundarywidget followed buy a ValueKey, would give more performance and reduce the renders.