Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active April 23, 2024 13:40
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save PlugFox/7cb6362f020363d1544495fa262aa6a1 to your computer and use it in GitHub Desktop.
Save PlugFox/7cb6362f020363d1544495fa262aa6a1 to your computer and use it in GitHub Desktop.
Flutter Shimmer & Skeleton
void main() => runZonedGuarded<void>(
() => runApp(const App()),
(error, stackTrace) => log('Top level exception $error'),
);
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Shimmer shader',
theme: ThemeData.light(),
home: Scaffold(
appBar: AppBar(
title: const Text('Shimmer shader'),
),
body: SafeArea(
child: Center(
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: const Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Shimmer(),
SizedBox(height: 8),
Shimmer(
size: Size(64, 28),
color: Colors.red,
backgroundColor: Colors.indigo,
speed: 25,
),
SizedBox(height: 8),
Shimmer(
size: Size.square(128),
cornerRadius: 48,
speed: 5,
color: Colors.amber,
),
SizedBox(height: 8),
Shimmer(
color: Colors.red,
backgroundColor: Colors.blue,
),
SizedBox(height: 8),
Shimmer(
size: Size.fromRadius(48),
cornerRadius: 32,
color: Colors.red,
),
SizedBox(height: 8),
Shimmer(
speed: 15,
stripeWidth: .1,
backgroundColor: Colors.amber,
),
],
),
),
),
),
),
),
);
}
# ...
flutter:
uses-material-design: true
shaders:
- assets/shaders/shimmer.frag
import 'dart:ui' as ui show FragmentProgram, FragmentShader;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.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 StatefulWidget {
/// {@macro shimmer}
const Shimmer({
this.color,
this.backgroundColor,
this.speed = 15,
this.stripeWidth = .2,
this.size = const Size(128, 28),
this.cornerRadius = 8,
this.initialSeed = .0,
super.key,
});
/// The color used for the shimmering effect,
/// usually a light color for contrast.
/// If not specified, defaults to the color
/// set in the current theme's `ColorScheme.onSurface`.
final Color? color;
/// The color of the widget's background.
/// Should typically contrast with [color].
/// If not specified, defaults to the color
/// set in the current theme's `ColorScheme.surface`.
final Color? backgroundColor;
/// The radius of the corners of the widget in logical pixels.
/// Defaults to 8 logical pixels.
final double cornerRadius;
/// The speed of the shimmering effect, in logical pixels per second.
/// Defaults to 0.015.
final double speed;
/// The width of the stripes in the shimmering effect,
/// expressed as a fraction of the widget's width.
/// Defaults to 0.1, meaning each stripe will be 1/10th of the widget's width.
final double stripeWidth;
/// The size of the widget in logical pixels.
/// Defaults to a size of 128 logical pixels wide and 28 logical pixels tall.
final Size size;
/// The initial offset of the shimmering effect.
/// Expressed as a fraction of the widget's width.
/// Defaults to 0.0,
/// meaning the shimmering effect starts at the leading edge of the widget.
final double initialSeed;
@override
State<Shimmer> createState() => _ShimmerState();
}
class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
/// Init shader.
static final Future<ui.FragmentShader?> _shaderFuture = ui.FragmentProgram.fromAsset('assets/shaders/shimmer.frag')
.then<ui.FragmentShader?>((program) => program.fragmentShader(), onError: (_, __) => null);
/// Seed value notifier for shader mutation.
late final ValueNotifier<double> _seed;
/// Animated ticker.
late final Ticker _ticker;
void _updateSeed(Duration elapsed) => _seed.value = elapsed.inMilliseconds * widget.speed / 8000;
@override
void initState() {
super.initState();
_seed = ValueNotifier<double>(widget.initialSeed);
_ticker = createTicker(_updateSeed)..start();
}
@override
void dispose() {
_ticker.dispose();
_seed.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => SizedBox.fromSize(
size: widget.size,
child: RepaintBoundary(
child: FutureBuilder<ui.FragmentShader?>(
initialData: null,
future: _shaderFuture,
builder: (context, snapshot) => CustomPaint(
size: widget.size,
painter: _ShimmerPainter(
shader: snapshot.data,
seed: _seed,
/* ?? Colors.grey */
color: widget.color ?? Theme.of(context).colorScheme.primary,
/* ?? Theme.of(context).colorScheme.surface */
backgroundColor: widget.backgroundColor ?? Theme.of(context).colorScheme.background,
cornerRadius: widget.cornerRadius,
stripeWidth: widget.stripeWidth,
),
),
),
),
);
}
class _ShimmerPainter extends CustomPainter {
_ShimmerPainter({
required this.seed,
required this.color,
required this.backgroundColor,
required this.stripeWidth,
required this.cornerRadius,
required this.shader,
}) : super(repaint: seed);
final ValueListenable<double> seed;
final Color color;
final Color backgroundColor;
final double stripeWidth;
final double cornerRadius;
final ui.FragmentShader? shader;
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
if (shader == null) return canvas.drawRect(rect, Paint()..color = backgroundColor);
final paint = Paint()
..shader = (shader!
..setFloat(0, size.width)
..setFloat(1, size.height)
..setFloat(2, seed.value)
..setFloat(3, color.red / 255)
..setFloat(4, color.green / 255)
..setFloat(5, color.blue / 255)
..setFloat(6, color.alpha / 255)
..setFloat(7, backgroundColor.red / 255)
..setFloat(8, backgroundColor.green / 255)
..setFloat(9, backgroundColor.blue / 255)
..setFloat(10, backgroundColor.alpha / 255)
..setFloat(11, stripeWidth));
canvas
..clipRRect(RRect.fromRectAndRadius(rect, Radius.circular(cornerRadius)))
..drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant _ShimmerPainter oldDelegate) =>
color != oldDelegate.color ||
backgroundColor != oldDelegate.backgroundColor ||
cornerRadius != oldDelegate.cornerRadius ||
stripeWidth != oldDelegate.stripeWidth;
}
#version 460 core
#define SHOW_GRID
#include <flutter/runtime_effect.glsl>
uniform vec2 uSize; // size of the shape
uniform float uSeed; // shader playback time (in seconds)
uniform vec4 uLineColor; // line color of the shape
uniform vec4 uBackgroundColor; // background color of the shape
uniform float uStripeWidth; // 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 / uSize;
// Generate a smooth moving wave based on time and coordinates
float waveRaw = 0.5 * (1.0 + sin(uSeed - dot(normalizedCoords, direction) * uStripeWidth * 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(uBackgroundColor, uLineColor, wave);
fragColor = color;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment