-
-
Save engladgut/03ed2f6cb6914df7125b2547116b528d to your computer and use it in GitHub Desktop.
Flutter Shimmer & Skeleton
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
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, | |
), | |
], | |
), | |
), | |
), | |
), | |
), | |
), | |
); | |
} |
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
# ... | |
flutter: | |
uses-material-design: true | |
shaders: | |
- assets/shaders/shimmer.frag |
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 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; | |
} |
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 | |
#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