Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active October 12, 2025 19:26
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.

Revisions

  1. PlugFox revised this gist Jun 24, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    ```
    .
    project
    └───packages
    └───ui
    ├───lib
  2. PlugFox revised this gist Jun 24, 2025. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,8 @@
    ├───lib
    │ ├───shaders
    │ │ └─── shimmer.frag
    │ └───src
    │ └─── shimmer.dart
    │ ├───src
    │ │ └─── shimmer.dart
    │ └───ui.dart
    └─── pubspec.yaml
    ```
  3. PlugFox revised this gist Jun 24, 2025. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    ```
    .
    └───packages
    └───ui
    @@ -7,3 +8,4 @@
    │ └───src
    │ └─── shimmer.dart
    └─── pubspec.yaml
    ```
  4. PlugFox created this gist Jun 24, 2025.
    9 changes: 9 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    .
    └───packages
    └───ui
    ├───lib
    │ ├───shaders
    │ │ └─── shimmer.frag
    │ └───src
    │ └─── shimmer.dart
    └─── pubspec.yaml
    15 changes: 15 additions & 0 deletions pubspec.yaml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    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
    308 changes: 308 additions & 0 deletions shimmer.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,308 @@
    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();
    }
    }
    }
    29 changes: 29 additions & 0 deletions shimmer.frag
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,29 @@
    #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;
    }