import 'package:flutter/material.dart'; /// Vertikales Drehrad zum Einstellen eines Werts per Swipe (HTML- /// Demonstrator: .wheel/#altWheel/#spdWheel + setupWheel()). Der Tick an /// der Bildschirmmitte (blauer Indikator) zeigt den aktuellen Wert; nach /// oben ziehen erhoeht den Wert. class ValueWheel extends StatefulWidget { const ValueWheel({ super.key, required this.value, required this.min, required this.max, required this.step, required this.pxPerStep, required this.majorEvery, required this.onChanged, }); final double value; final double min; final double max; final double step; final double pxPerStep; final int majorEvery; final ValueChanged onChanged; @override State createState() => _ValueWheelState(); } class _ValueWheelState extends State { double? _dragStartY; double _dragStartValue = 0; void _onDragStart(DragStartDetails details) { _dragStartY = details.globalPosition.dy; _dragStartValue = widget.value; } void _onDragUpdate(DragUpdateDetails details) { final startY = _dragStartY; if (startY == null) return; final deltaY = startY - details.globalPosition.dy; final deltaSteps = deltaY / widget.pxPerStep; final raw = _dragStartValue + deltaSteps * widget.step; var stepped = (raw / widget.step).round() * widget.step; stepped = stepped.clamp(widget.min, widget.max); widget.onChanged(stepped); } @override Widget build(BuildContext context) { return GestureDetector( onVerticalDragStart: _onDragStart, onVerticalDragUpdate: _onDragUpdate, onVerticalDragEnd: (_) => _dragStartY = null, child: Container( width: 44, decoration: BoxDecoration( borderRadius: BorderRadius.circular(22), gradient: LinearGradient( colors: [ Colors.black.withValues(alpha: 0.9), const Color(0xFF333333).withValues(alpha: 0.9), Colors.black.withValues(alpha: 0.9), ], ), ), child: ClipRRect( borderRadius: BorderRadius.circular(22), child: ShaderMask( blendMode: BlendMode.dstIn, shaderCallback: (rect) => const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black, Colors.black, Colors.transparent, ], stops: [0, 0.1, 0.9, 1], ).createShader(rect), child: Stack( alignment: Alignment.center, children: [ Positioned.fill( child: CustomPaint( painter: _TapePainter( value: widget.value, min: widget.min, max: widget.max, step: widget.step, pxPerStep: widget.pxPerStep, majorEvery: widget.majorEvery, ), ), ), Container(height: 2, color: const Color(0xFF4D9DFF)), ], ), ), ), ), ); } } class _TapePainter extends CustomPainter { const _TapePainter({ required this.value, required this.min, required this.max, required this.step, required this.pxPerStep, required this.majorEvery, }); final double value; final double min; final double max; final double step; final double pxPerStep; final int majorEvery; @override void paint(Canvas canvas, Size size) { final centerY = size.height / 2; final centerX = size.width / 2; final majorPaint = Paint()..color = Colors.white.withValues(alpha: 0.85); final minorPaint = Paint()..color = Colors.white.withValues(alpha: 0.32); final nearestGridIndex = ((value - min) / step).round(); final visibleHalfCount = (size.height / 2 / pxPerStep).ceil() + 1; for (var i = -visibleHalfCount; i <= visibleHalfCount; i++) { final gridIndex = nearestGridIndex + i; final tickValue = min + gridIndex * step; if (tickValue < min || tickValue > max) continue; final y = centerY + (tickValue - value) / step * pxPerStep; final isMajor = gridIndex % majorEvery == 0; final halfWidth = size.width * (isMajor ? 0.35 : 0.19); final thickness = isMajor ? 2.0 : 1.0; canvas.drawRect( Rect.fromCenter( center: Offset(centerX, y), width: halfWidth * 2, height: thickness, ), isMajor ? majorPaint : minorPaint, ); } } @override bool shouldRepaint(covariant _TapePainter oldDelegate) => oldDelegate.value != value || oldDelegate.min != min || oldDelegate.max != max || oldDelegate.step != step; }