feat: everything up to bluetooth scanning
This commit is contained in:
117
lib/widgets/device_listitem.dart
Normal file
117
lib/widgets/device_listitem.dart
Normal file
@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui'; // Required for ImageFilter
|
||||
|
||||
class DeviceListItem extends StatelessWidget {
|
||||
final String deviceName;
|
||||
final String deviceId; // Added for potential future use or subtitle
|
||||
final bool isUnknownDevice;
|
||||
// final String? imageUrl; // Optional image URL - commented out for now
|
||||
|
||||
const DeviceListItem({
|
||||
super.key,
|
||||
required this.deviceName,
|
||||
required this.deviceId,
|
||||
this.isUnknownDevice = false,
|
||||
// this.imageUrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDarkMode = theme.brightness == Brightness.dark;
|
||||
|
||||
// Glassy effect colors - adjust transparency and base color as needed
|
||||
final glassColor = isDarkMode
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.black.withOpacity(0.05);
|
||||
final shadowColor = isDarkMode
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.grey.withOpacity(0.5);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left side: Rounded Square Container
|
||||
Container(
|
||||
width: 60, // Square size
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12), // Rounded corners
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: shadowColor,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
// Apply blur effect within rounded corners
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
glassColor, // Semi-transparent color for glass effect
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2), // Subtle border
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
// Placeholder '?' - replace with Image widget when imageUrl is available
|
||||
child: Text(
|
||||
'?',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white70, // Adjust color as needed
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16), // Spacing between image and text
|
||||
|
||||
// Right side: Device Name and ID
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isUnknownDevice ? 'Unknown Device' : deviceName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
isUnknownDevice ? FontWeight.normal : FontWeight.w500,
|
||||
fontStyle:
|
||||
isUnknownDevice ? FontStyle.italic : FontStyle.normal,
|
||||
color: isUnknownDevice
|
||||
? theme.hintColor
|
||||
: theme.textTheme.bodyLarge?.color,
|
||||
),
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Prevent long names from overflowing
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
deviceId, // Display device ID as subtitle
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.hintColor),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Optional: Add an icon or button on the far right if needed later
|
||||
// Icon(Icons.chevron_right, color: theme.hintColor),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
187
lib/widgets/scanning_animation.dart
Normal file
187
lib/widgets/scanning_animation.dart
Normal file
@ -0,0 +1,187 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScanningWaveAnimation extends StatefulWidget {
|
||||
final Animation<double>
|
||||
animation; // For the wave effect (0.0 to 1.0 over 1.5s)
|
||||
final double
|
||||
progressValue; // For the CircularProgressIndicator (0.0 to 1.0 over scan duration)
|
||||
final Color waveColor; // Color for the scanning wave
|
||||
|
||||
const ScanningWaveAnimation({
|
||||
super.key,
|
||||
required this.animation,
|
||||
required this.progressValue,
|
||||
this.waveColor = Colors.lightBlueAccent, // Default color using constant
|
||||
});
|
||||
|
||||
@override
|
||||
_ScanningWaveAnimationState createState() => _ScanningWaveAnimationState();
|
||||
}
|
||||
|
||||
class _ScanningWaveAnimationState extends State<ScanningWaveAnimation> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Stack to layer the painter, progress indicator, and text
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// The wave animation painter rebuilds based on the wave animation
|
||||
AnimatedBuilder(
|
||||
animation: widget.animation,
|
||||
builder: (context, child) {
|
||||
return CustomPaint(
|
||||
// Use full available size for the painter
|
||||
size: Size.infinite,
|
||||
painter: _WavePainter(
|
||||
progress: widget.animation.value, // Pass wave progress
|
||||
waveColor: widget.waveColor), // Pass wave color
|
||||
);
|
||||
},
|
||||
),
|
||||
// The progress indicator and text in the center
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min, // Keep column compact
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: widget.progressValue, // Use the scan duration progress
|
||||
backgroundColor:
|
||||
Colors.white.withValues(alpha: 0.1), // Subtle background
|
||||
),
|
||||
const SizedBox(height: 16), // Consistent spacing
|
||||
const Text(
|
||||
'Scanning for devices...',
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: Colors.white70), // Lighter text
|
||||
),
|
||||
], // Close children[] of Column
|
||||
), // Close Column
|
||||
], // Close children[] of Stack
|
||||
); // Close Stack
|
||||
}
|
||||
}
|
||||
|
||||
// --- New Wave Painter Implementation ---
|
||||
class _WavePainter extends CustomPainter {
|
||||
final double
|
||||
progress; // Animation value from 0.0 to 1.0, drives the single wave
|
||||
final Color waveColor; // The color of the wave
|
||||
final double startRadius = 50.0; // Start wave ~175px from center
|
||||
final double waveThickness = 10.0; // Thickness of the wave
|
||||
final double waveExpansion = 50.0; // Amount of thickness increase at the end
|
||||
|
||||
_WavePainter({required this.progress, required this.waveColor});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
// Max radius should reach significantly beyond the screen edge
|
||||
final maxRadius = min(size.width, size.height) * 0.8; // Extend further
|
||||
// Ensure endRadius is always larger than startRadius
|
||||
final endRadius = max(startRadius + 1.0, maxRadius);
|
||||
|
||||
// Apply deceleration curve to the progress
|
||||
final easedProgress = Curves.easeOut.transform(progress);
|
||||
|
||||
final realWaveThickness = waveThickness + waveExpansion * easedProgress;
|
||||
|
||||
// --- Opacity Calculation (Fade-in / Fade-out) ---
|
||||
const double fadeInEnd = 0.20; // Faster fade-in (0% to 20% of animation)
|
||||
const double fadeOutStart = 0.45; // Start fade-out earlier (45% to 100%)
|
||||
|
||||
final double fadeInOpacity =
|
||||
(progress < fadeInEnd) ? progress / fadeInEnd : 1.0;
|
||||
|
||||
final double fadeOutProgress = (progress < fadeOutStart)
|
||||
? 0.0 // Not fading out yet
|
||||
: (progress - fadeOutStart) /
|
||||
(1.0 - fadeOutStart); // Map fade-out phase to 0.0-1.0
|
||||
final double fadeOutOpacity =
|
||||
max(0.0, 1.0 - fadeOutProgress); // Linear fade-out
|
||||
|
||||
// Combine fade-in and fade-out using minimum
|
||||
final opacity = max(0.0, min(fadeInOpacity, fadeOutOpacity));
|
||||
|
||||
// Skip drawing if fully faded out
|
||||
if (opacity <= 0.001) {
|
||||
// Use a small threshold to avoid floating point issues
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Base Radius Calculation ---
|
||||
// Calculate the base radius for this frame based on decelerated progress
|
||||
final baseRadius = startRadius + (endRadius - startRadius) * easedProgress;
|
||||
|
||||
// Skip drawing if radius hasn't reached startRadius yet
|
||||
// (Needed because easedProgress might be 0 at the very start)
|
||||
if (baseRadius < startRadius) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Path Generation with Deformation ---
|
||||
final path = Path();
|
||||
final int steps = 200; // Increase steps for smoother deformation
|
||||
final double angleStep = (2 * pi) / steps;
|
||||
|
||||
// Noise parameters (tune these for desired 'waviness' and 'shimmer')
|
||||
final double noiseMaxAmplitude =
|
||||
realWaveThickness * 0.4; // Max radius deviation
|
||||
final double noiseAmplitude = noiseMaxAmplitude *
|
||||
min(easedProgress * 4.0,
|
||||
1.0 - easedProgress * 0.8); // Amplitude decreases as it expands
|
||||
final double noiseFreq1 = 6.0; // Controls number of 'bumps'
|
||||
final double noiseFreq2 = 12.0; // Another layer of bumps
|
||||
final double phaseShift = progress * pi * 6; // Controls 'shimmer' speed
|
||||
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
final double angle = i * angleStep;
|
||||
|
||||
// Calculate noise perturbation using layered sine waves
|
||||
// Different frequencies and phase shifts create complex patterns
|
||||
double perturbation = noiseAmplitude *
|
||||
(0.6 * sin(noiseFreq1 * angle + phaseShift) +
|
||||
0.4 *
|
||||
sin(noiseFreq2 * angle -
|
||||
phaseShift * 0.6) // Opposite phase shift adds complexity
|
||||
);
|
||||
|
||||
final double currentRadius = max(
|
||||
0.0, baseRadius + perturbation); // Ensure radius doesn't go negative
|
||||
|
||||
// Convert polar (angle, radius) to cartesian (x, y)
|
||||
final double x = center.dx + currentRadius * cos(angle);
|
||||
final double y = center.dy + currentRadius * sin(angle);
|
||||
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y); // Start the path
|
||||
} else {
|
||||
path.lineTo(x, y); // Draw line to next point
|
||||
}
|
||||
}
|
||||
path.close(); // Connect the last point back to the first
|
||||
|
||||
// --- Painting the Path ---
|
||||
// Use the provided wave color with calculated opacity
|
||||
final paintColor = waveColor.withValues(alpha: opacity * 0.75);
|
||||
|
||||
final paint = Paint()
|
||||
..color = paintColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = realWaveThickness
|
||||
..strokeCap =
|
||||
StrokeCap.round // Soften line endings (though path is closed)
|
||||
..strokeJoin = StrokeJoin.round // Soften corners in the deformation
|
||||
// Apply blur for feathered/soft edges, sigma related to thickness
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, realWaveThickness * 0.5);
|
||||
|
||||
// Draw the deformed, blurred path
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _WavePainter oldDelegate) {
|
||||
// Repaint whenever the animation progress changes
|
||||
return oldDelegate.progress != progress ||
|
||||
oldDelegate.waveColor != waveColor;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user