Skip to content

Instantly share code, notes, and snippets.

@roipeker
Last active June 3, 2025 12:30
Show Gist options
  • Save roipeker/051379b9680f93391ed3912af9263a52 to your computer and use it in GitHub Desktop.
Save roipeker/051379b9680f93391ed3912af9263a52 to your computer and use it in GitHub Desktop.
File scanner animation
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'File Scanner Animation',
debugShowCheckedModeBanner: false,
home: LabrysScannerDemo(),
);
}
}
class LabrysScannerDemo extends StatelessWidget {
const LabrysScannerDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: SizedBox(
width: 120,
height: 80,
child: LabrysScannerThumb(
style: ScannerStyle.beam,
color: Colors.cyanAccent,
intensity: 0.3,
// instead of alpha
enableGlow: true,
glowRadius: 12,
borderRadius: BorderRadius.circular(8),
child: LabrysScannerThumb.placeholder('Pantelis 🪄'),
),
),
),
);
}
}
// -- implementation
enum ScannerStyle {
beam, // Focused beam effect (linear)
laser, // Sharp line
gradient, // gradient sweep
}
// "Enhanced" scanner with style options (subtle gradient/curve changes though)
class LabrysScannerThumb extends StatefulWidget {
static Widget placeholder(String label) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[800],
border: Border.all(color: Colors.grey[600]!, width: 1),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.image, size: 40, color: Colors.grey[400]),
SizedBox(height: 8),
Text(
label,
style: TextStyle(color: Colors.grey[400], fontSize: 12),
textAlign: TextAlign.center,
),
],
),
),
);
}
final Widget child;
final Duration scanDuration;
final Color color;
// Instead of Color.alpha, as we use the base color for the glow too
final double intensity;
final ScannerStyle style;
// "hi-tech" view, why not
final bool enableGlow;
final double glowRadius;
// for clipping
final BorderRadius? borderRadius;
const LabrysScannerThumb({
super.key,
required this.child,
this.scanDuration = const Duration(milliseconds: 2000),
this.color = Colors.cyan,
this.intensity = 0.6,
this.style = ScannerStyle.beam,
this.enableGlow = false,
this.glowRadius = 8.0,
this.borderRadius,
});
@override
State<LabrysScannerThumb> createState() => _LabrysScannerThumbState();
}
class _LabrysScannerThumbState extends State<LabrysScannerThumb>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.scanDuration,
vsync: this,
);
_animation = Tween<double>(
begin: -1.2,
end: 1.2,
).animate(CurvedAnimation(parent: _controller, curve: _getCurveForStyle()));
_controller.repeat();
}
Curve _getCurveForStyle() {
switch (widget.style) {
case ScannerStyle.beam:
return Curves.linear;
case ScannerStyle.laser:
return Curves.linear;
case ScannerStyle.gradient:
return Curves.easeInOutCubic;
}
}
List<Color> _getColorsForStyle() {
final base = widget.color;
final transparent = Colors.transparent;
switch (widget.style) {
case ScannerStyle.beam:
return [
transparent,
base.withValues(alpha: 0.1 * widget.intensity),
base.withValues(alpha: 0.8 * widget.intensity),
base.withValues(alpha: 0.1 * widget.intensity),
transparent,
];
case ScannerStyle.laser:
return [
transparent,
base.withValues(alpha: widget.intensity),
transparent,
];
case ScannerStyle.gradient:
return [
transparent,
base.withValues(alpha: 0.3 * widget.intensity),
base.withValues(alpha: 0.7 * widget.intensity),
base.withValues(alpha: 0.3 * widget.intensity),
transparent,
];
}
}
List<double> _getStopsForStyle() {
switch (widget.style) {
case ScannerStyle.beam:
return [0.0, 0.2, 0.5, 0.8, 1.0];
case ScannerStyle.laser:
return [0.0, 0.5, 1.0];
case ScannerStyle.gradient:
return [0.0, 0.25, 0.5, 0.75, 1.0];
}
}
double _getWidthForStyle() {
switch (widget.style) {
case ScannerStyle.beam:
return 0.3;
case ScannerStyle.laser:
return 0.1;
case ScannerStyle.gradient:
return 0.4;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final width = _getWidthForStyle();
Widget scanner = Container(
clipBehavior: widget.borderRadius != null
? Clip.antiAlias
: Clip.none,
decoration: BoxDecoration(),
foregroundDecoration: BoxDecoration(
borderRadius: widget.borderRadius,
gradient: LinearGradient(
colors: _getColorsForStyle(),
stops: _getStopsForStyle(),
begin: Alignment(0, _animation.value - width),
end: Alignment(0, _animation.value + width),
),
),
child: widget.child,
);
if (widget.enableGlow) {
// very very shitty.
return Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: widget.color.withValues(alpha: 0.3),
blurRadius: widget.glowRadius,
spreadRadius: 2,
),
],
),
child: scanner,
);
}
return scanner;
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment