Skip to content

Instantly share code, notes, and snippets.

@callmephil
Created May 13, 2025 19:50
Show Gist options
  • Save callmephil/846f51bc8341a40959fe1c8f0bfb7f5a to your computer and use it in GitHub Desktop.
Save callmephil/846f51bc8341a40959fe1c8f0bfb7f5a to your computer and use it in GitHub Desktop.
Scanner Effect
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Scanning Effect Demo',
home: ScannerEffectScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class ScannerEffectScreen extends StatefulWidget {
const ScannerEffectScreen({Key? key}) : super(key: key);
@override
_ScannerEffectScreenState createState() => _ScannerEffectScreenState();
}
class _ScannerEffectScreenState extends State<ScannerEffectScreen>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
final Interval _expandInterval = const Interval(
0.0,
0.15,
curve: Curves.easeOut,
);
final Interval _scanDownInterval = const Interval(
0.15,
0.85,
curve: Curves.decelerate,
);
final Interval _collapseInterval = const Interval(
0.85,
1.0,
curve: Curves.easeIn,
);
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..forward();
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {});
_animationController.repeat(); // Restart the animation on completion
}
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const double scanAreaWidthRatio = .7;
const double scanAreaAspectRatio = .8;
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Center(
child: AspectRatio(
aspectRatio: scanAreaWidthRatio,
child: Image.network('https://picsum.photos/800'),
),
),
Center(
child: AspectRatio(
aspectRatio: scanAreaAspectRatio,
child: LayoutBuilder(
builder: (context, constraints) {
final scanAreaSize = constraints.biggest;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return CustomPaint(
painter: ScannerEffectPainter(
animationValue: _animationController.value,
scanAreaSize: scanAreaSize,
expandInterval: _expandInterval,
scanDownInterval: _scanDownInterval,
collapseInterval: _collapseInterval,
),
);
},
);
},
),
),
),
],
),
);
}
}
class ScannerEffectPainter extends CustomPainter {
final double animationValue;
final Size scanAreaSize;
final Interval expandInterval;
final Interval scanDownInterval;
final Interval collapseInterval;
ScannerEffectPainter({
required this.animationValue,
required this.scanAreaSize,
required this.expandInterval,
required this.scanDownInterval,
required this.collapseInterval,
});
final Paint framePaint =
Paint()
..color = Colors.white54
..style = PaintingStyle.stroke
..strokeWidth = 1;
final Paint linePaint =
Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 5.0;
Paint getTrailPaint(double animationValue) {
// Modify alpha based on animationValue, in reverse from 0.3 to 0.0
double alpha = 0.1 - (animationValue * 0.1);
if (alpha < 0) alpha = 0;
return Paint()
..color = Colors.black54.withValues(alpha: alpha)
..style = PaintingStyle.fill;
}
Paint getGridPaint(double animationValue) {
double alpha = 0.1;
if (animationValue >= collapseInterval.begin) {
// Fade out the grid alpha during the collapse interval
alpha = Tween<double>(begin: 0.1, end: 0.0).transform(
Curves.linear.transform(
(animationValue - collapseInterval.begin) /
(collapseInterval.end - collapseInterval.begin),
),
);
}
return Paint()
..color = Colors.white54.withAlpha((alpha * 255).toInt())
..style = PaintingStyle.stroke
..strokeWidth = .5;
}
final int gridRows = 8;
final int gridColumns = 6;
@override
void paint(Canvas canvas, Size size) {
final Rect scanRect = Rect.fromLTWH(
0,
0,
scanAreaSize.width,
scanAreaSize.height,
);
final RRect frameRRect = RRect.fromRectAndRadius(
scanRect,
const Radius.circular(8.0),
);
canvas.drawRRect(frameRRect, framePaint);
const double bracketSize = 20.0;
const double bracketStrokeWidth = 3.0;
final Paint bracketPaint =
Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = bracketStrokeWidth;
canvas.drawLine(
scanRect.topLeft + const Offset(0, bracketSize),
scanRect.topLeft,
bracketPaint,
);
canvas.drawLine(
scanRect.topLeft + const Offset(bracketSize, 0),
scanRect.topLeft,
bracketPaint,
);
canvas.drawLine(
scanRect.topRight - const Offset(0, -bracketSize),
scanRect.topRight,
bracketPaint,
);
canvas.drawLine(
scanRect.topRight - const Offset(bracketSize, 0),
scanRect.topRight,
bracketPaint,
);
double currentHorizontalWidth;
if (animationValue < expandInterval.end) {
currentHorizontalWidth =
Tween<double>(begin: 0.0, end: scanAreaSize.width)
.animate(
CurvedAnimation(
parent: AlwaysStoppedAnimation(animationValue),
curve: expandInterval,
),
)
.value;
} else if (animationValue >= expandInterval.end &&
animationValue <= scanDownInterval.end) {
currentHorizontalWidth = scanAreaSize.width;
} else {
currentHorizontalWidth =
Tween<double>(begin: scanAreaSize.width, end: 0.0)
.animate(
CurvedAnimation(
parent: AlwaysStoppedAnimation(animationValue),
curve: collapseInterval,
),
)
.value;
}
double currentVerticalPosition;
if (animationValue < scanDownInterval.begin) {
currentVerticalPosition = scanRect.top;
} else if (animationValue >= scanDownInterval.begin &&
animationValue <= scanDownInterval.end) {
currentVerticalPosition =
Tween<double>(begin: scanRect.top, end: scanRect.bottom)
.animate(
CurvedAnimation(
parent: AlwaysStoppedAnimation(animationValue),
curve: scanDownInterval,
),
)
.value;
} else {
currentVerticalPosition = scanRect.bottom;
}
if (animationValue >= scanDownInterval.begin &&
animationValue <= scanDownInterval.end) {
final Rect trailRect = Rect.fromLTWH(
scanRect.left,
scanRect.top,
scanRect.width,
currentVerticalPosition,
);
canvas.drawRect(trailRect, getTrailPaint(animationValue));
}
final double scanLineLeft =
(scanAreaSize.width - currentHorizontalWidth) / 2.0;
final double scanLineRight = scanLineLeft + currentHorizontalWidth;
canvas.drawLine(
Offset(scanLineLeft, currentVerticalPosition),
Offset(scanLineRight, currentVerticalPosition),
linePaint,
);
// Grid drawing logic:
if (animationValue <= collapseInterval.begin) {
final double horizontalSpacing = scanRect.width / gridColumns;
final double verticalSpacing = scanRect.height / gridRows;
final gridPaint = getGridPaint(animationValue);
for (int i = 0; i <= gridColumns; i++) {
final double x = scanRect.left + i * horizontalSpacing;
canvas.drawLine(
Offset(x, scanRect.top),
Offset(x, min(currentVerticalPosition, scanRect.bottom)),
gridPaint,
);
}
for (int i = 1; i < gridRows; i++) {
final double y = scanRect.top + i * verticalSpacing;
if (currentVerticalPosition >= y) {
canvas.drawLine(
Offset(scanRect.left, y),
Offset(scanRect.right, y),
gridPaint,
);
}
}
}
}
@override
bool shouldRepaint(covariant ScannerEffectPainter oldDelegate) {
return oldDelegate.animationValue != animationValue;
}
}
@callmephil
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment