Last active
June 3, 2025 12:30
-
-
Save roipeker/051379b9680f93391ed3912af9263a52 to your computer and use it in GitHub Desktop.
File scanner animation
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/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