Created
May 13, 2025 19:50
-
-
Save callmephil/846f51bc8341a40959fe1c8f0bfb7f5a to your computer and use it in GitHub Desktop.
Scanner Effect
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: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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
attempt at cloning this https://dribbble.com/shots/24956389-AI-Chef-Mobile-App-Animation