TempoFlow/lib/widgets/rotary_knob.dart

165 lines
4.7 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_theme.dart';
import '../models/app_settings.dart';
class RotaryKnob extends StatelessWidget {
final double size;
final double value;
final double min;
final double max;
final bool isTiming;
final double currentSeconds;
final ValueChanged<double> onChanged;
const RotaryKnob({
super.key,
required this.size,
required this.value,
required this.min,
required this.max,
required this.isTiming,
required this.currentSeconds,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final displaySeconds = isTiming ? currentSeconds.toInt() : (value * 60).toInt();
final minutesStr = (displaySeconds / 60).floor().toString().padLeft(2, '0');
final secondsStr = (displaySeconds % 60).toString().padLeft(2, '0');
final double progress = isTiming
? currentSeconds / (value * 60)
: (value - min) / (max - min);
return GestureDetector(
onPanUpdate: (details) {
if (isTiming) return;
double sensitivity = 0.5;
double newValue = value - (details.delta.dy * sensitivity);
newValue = newValue.clamp(min, max);
if (newValue.round() != value.round() && AppSettings.enableHaptics) {
HapticFeedback.selectionClick();
}
onChanged(newValue);
},
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.bgLight,
boxShadow: isTiming ? [] : AppTheme.softShadow,
),
child: CustomPaint(
painter: KnobProgressPainter(
progress: progress,
color: isTiming ? AppTheme.flowStart : AppTheme.primary,
isTiming: isTiming,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
minutesStr,
style: TextStyle(
fontSize: size * 0.32,
fontWeight: FontWeight.w700,
color: isTiming ? Colors.white : AppTheme.primary,
height: 1.0,
letterSpacing: -2.0,
shadows: isTiming
? [const Shadow(blurRadius: 8, color: Colors.black54)]
: null,
),
),
Text(
secondsStr,
style: TextStyle(
fontSize: size * 0.12,
fontWeight: FontWeight.w600,
color: isTiming ? Colors.white70 : AppTheme.textSub,
letterSpacing: 1.0,
),
),
if (!isTiming)
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text(
"DRAG TO SET",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
letterSpacing: 2,
color: Colors.grey,
),
),
)
],
),
),
),
),
);
}
}
class KnobProgressPainter extends CustomPainter {
final double progress;
final Color color;
final bool isTiming;
KnobProgressPainter({
required this.progress,
required this.color,
required this.isTiming,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 10;
final trackPaint = Paint()
..color = Colors.grey.withOpacity(0.2)
..style = PaintingStyle.stroke
..strokeWidth = 10;
canvas.drawCircle(center, radius, trackPaint);
final progressPaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 10
..strokeCap = StrokeCap.round;
final sweepAngle = 2 * math.pi * (isTiming ? progress : (progress * 0.75));
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-math.pi / 2,
sweepAngle,
false,
progressPaint,
);
if (!isTiming) {
final knobAngle = -math.pi / 2 + sweepAngle;
final knobCenter = Offset(
center.dx + radius * math.cos(knobAngle),
center.dy + radius * math.sin(knobAngle),
);
canvas.drawCircle(knobCenter, 14, Paint()..color = Colors.white);
canvas.drawCircle(knobCenter, 5, Paint()..color = color);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}