Skip to content

Commit a966c39

Browse files
committed
draft
1 parent 82466ed commit a966c39

4 files changed

Lines changed: 428 additions & 147 deletions

File tree

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
import 'dart:ui';
2+
3+
import 'package:flutter/material.dart';
4+
5+
enum PlayerAdjustmentHudType {
6+
brightness,
7+
volume,
8+
}
9+
10+
class PlayerAdjustmentHud extends StatefulWidget {
11+
const PlayerAdjustmentHud({
12+
super.key,
13+
required this.visible,
14+
required this.type,
15+
required this.value,
16+
this.disableAnimations = false,
17+
});
18+
19+
final bool visible;
20+
final PlayerAdjustmentHudType type;
21+
final double value;
22+
final bool disableAnimations;
23+
24+
@override
25+
State<PlayerAdjustmentHud> createState() => _PlayerAdjustmentHudState();
26+
}
27+
28+
class _PlayerAdjustmentHudState extends State<PlayerAdjustmentHud> {
29+
late PlayerAdjustmentHudType _displayType;
30+
late double _displayValue;
31+
bool _snapProgressOnNextBuild = false;
32+
33+
@override
34+
void initState() {
35+
super.initState();
36+
_displayType = widget.type;
37+
_displayValue = widget.value;
38+
}
39+
40+
@override
41+
void didUpdateWidget(covariant PlayerAdjustmentHud oldWidget) {
42+
super.didUpdateWidget(oldWidget);
43+
if (widget.visible) {
44+
_displayType = widget.type;
45+
_displayValue = widget.value;
46+
if (!oldWidget.visible) {
47+
_snapProgressOnNextBuild = true;
48+
}
49+
}
50+
}
51+
52+
double get _progress {
53+
return switch (_displayType) {
54+
PlayerAdjustmentHudType.brightness =>
55+
_displayValue.clamp(0.0, 1.0).toDouble(),
56+
PlayerAdjustmentHudType.volume =>
57+
(_displayValue / 100).clamp(0.0, 1.0).toDouble(),
58+
};
59+
}
60+
61+
int get _percent => (_progress * 100).round();
62+
63+
IconData get _icon {
64+
if (_displayType == PlayerAdjustmentHudType.brightness) {
65+
if (_percent <= 8) {
66+
return Icons.brightness_low_rounded;
67+
}
68+
if (_percent < 55) {
69+
return Icons.brightness_medium_rounded;
70+
}
71+
return Icons.brightness_high_rounded;
72+
}
73+
if (_percent <= 0) {
74+
return Icons.volume_off_rounded;
75+
}
76+
if (_percent < 45) {
77+
return Icons.volume_down_rounded;
78+
}
79+
return Icons.volume_up_rounded;
80+
}
81+
82+
String get _label {
83+
return switch (_displayType) {
84+
PlayerAdjustmentHudType.brightness => '亮度',
85+
PlayerAdjustmentHudType.volume => '音量',
86+
};
87+
}
88+
89+
Color _accent(ColorScheme colorScheme) {
90+
return switch (_displayType) {
91+
PlayerAdjustmentHudType.brightness => colorScheme.tertiary,
92+
PlayerAdjustmentHudType.volume => colorScheme.primary,
93+
};
94+
}
95+
96+
Color _container(ColorScheme colorScheme) {
97+
return switch (_displayType) {
98+
PlayerAdjustmentHudType.brightness => colorScheme.tertiaryContainer,
99+
PlayerAdjustmentHudType.volume => colorScheme.primaryContainer,
100+
};
101+
}
102+
103+
Color _onContainer(ColorScheme colorScheme) {
104+
return switch (_displayType) {
105+
PlayerAdjustmentHudType.brightness => colorScheme.onTertiaryContainer,
106+
PlayerAdjustmentHudType.volume => colorScheme.onPrimaryContainer,
107+
};
108+
}
109+
110+
@override
111+
Widget build(BuildContext context) {
112+
final colorScheme = Theme.of(context).colorScheme;
113+
final accent = _accent(colorScheme);
114+
final container = _container(colorScheme);
115+
final onContainer = _onContainer(colorScheme);
116+
final surface = colorScheme.surfaceContainerHighest.withValues(alpha: 0.74);
117+
final border = colorScheme.outlineVariant.withValues(alpha: 0.34);
118+
final duration = widget.disableAnimations
119+
? Duration.zero
120+
: const Duration(milliseconds: 280);
121+
final snapProgress = _snapProgressOnNextBuild;
122+
if (snapProgress) {
123+
WidgetsBinding.instance.addPostFrameCallback((_) {
124+
if (!mounted) {
125+
return;
126+
}
127+
setState(() {
128+
_snapProgressOnNextBuild = false;
129+
});
130+
});
131+
}
132+
133+
return IgnorePointer(
134+
child: AnimatedOpacity(
135+
opacity: widget.visible ? 1 : 0,
136+
duration: duration,
137+
curve: Curves.easeOutCubic,
138+
child: AnimatedSlide(
139+
offset: widget.visible ? Offset.zero : const Offset(0, -0.18),
140+
duration: duration,
141+
curve: Curves.easeOutCubic,
142+
child: AnimatedScale(
143+
scale: widget.visible ? 1 : 0.92,
144+
duration: duration,
145+
curve: Curves.easeOutBack,
146+
child: ClipRRect(
147+
borderRadius: BorderRadius.circular(28),
148+
child: BackdropFilter(
149+
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
150+
child: AnimatedContainer(
151+
duration: duration,
152+
curve: Curves.easeOutCubic,
153+
width: 236,
154+
padding:
155+
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
156+
decoration: BoxDecoration(
157+
color: surface,
158+
borderRadius: BorderRadius.circular(28),
159+
border: Border.all(color: border),
160+
boxShadow: [
161+
BoxShadow(
162+
color: accent.withValues(
163+
alpha: widget.visible ? 0.24 : 0,
164+
),
165+
blurRadius: 32,
166+
spreadRadius: 1,
167+
),
168+
BoxShadow(
169+
color: Colors.black.withValues(alpha: 0.28),
170+
blurRadius: 24,
171+
offset: const Offset(0, 12),
172+
),
173+
],
174+
),
175+
child: Row(
176+
mainAxisSize: MainAxisSize.min,
177+
children: [
178+
AnimatedContainer(
179+
duration: duration,
180+
curve: Curves.easeOutCubic,
181+
width: 42,
182+
height: 42,
183+
decoration: BoxDecoration(
184+
color: container.withValues(alpha: 0.92),
185+
borderRadius: BorderRadius.circular(21),
186+
),
187+
child: AnimatedSwitcher(
188+
duration: duration,
189+
switchInCurve: Curves.easeOutBack,
190+
switchOutCurve: Curves.easeInCubic,
191+
transitionBuilder: (child, animation) {
192+
return ScaleTransition(
193+
scale: animation,
194+
child: FadeTransition(
195+
opacity: animation,
196+
child: child,
197+
),
198+
);
199+
},
200+
child: Icon(
201+
_icon,
202+
key: ValueKey(_icon),
203+
color: onContainer,
204+
size: 24,
205+
),
206+
),
207+
),
208+
const SizedBox(width: 12),
209+
Expanded(
210+
child: Column(
211+
mainAxisSize: MainAxisSize.min,
212+
crossAxisAlignment: CrossAxisAlignment.start,
213+
children: [
214+
Row(
215+
children: [
216+
Expanded(
217+
child: Text(
218+
_label,
219+
maxLines: 1,
220+
overflow: TextOverflow.ellipsis,
221+
style: TextStyle(
222+
color: colorScheme.onSurface
223+
.withValues(alpha: 0.88),
224+
fontSize: 12,
225+
fontWeight: FontWeight.w600,
226+
),
227+
),
228+
),
229+
Text(
230+
'$_percent%',
231+
style: TextStyle(
232+
color: colorScheme.onSurface,
233+
fontSize: 13,
234+
fontFeatures: const [
235+
FontFeature.tabularFigures(),
236+
],
237+
fontWeight: FontWeight.w700,
238+
),
239+
),
240+
],
241+
),
242+
const SizedBox(height: 8),
243+
TweenAnimationBuilder<double>(
244+
tween: Tween<double>(
245+
end: _progress,
246+
),
247+
duration: widget.disableAnimations || snapProgress
248+
? Duration.zero
249+
: const Duration(milliseconds: 360),
250+
curve: Curves.easeOutCubic,
251+
builder: (context, animatedProgress, _) {
252+
return SizedBox(
253+
width: double.infinity,
254+
height: 16,
255+
child: CustomPaint(
256+
painter: _AdjustmentTrackPainter(
257+
progress: animatedProgress,
258+
accent: accent,
259+
trackColor: colorScheme
260+
.surfaceContainerHighest
261+
.withValues(alpha: 0.86),
262+
tickColor: colorScheme.onSurfaceVariant
263+
.withValues(alpha: 0.18),
264+
),
265+
),
266+
);
267+
},
268+
),
269+
],
270+
),
271+
),
272+
],
273+
),
274+
),
275+
),
276+
),
277+
),
278+
),
279+
),
280+
);
281+
}
282+
}
283+
284+
class _AdjustmentTrackPainter extends CustomPainter {
285+
const _AdjustmentTrackPainter({
286+
required this.progress,
287+
required this.accent,
288+
required this.trackColor,
289+
required this.tickColor,
290+
});
291+
292+
final double progress;
293+
final Color accent;
294+
final Color trackColor;
295+
final Color tickColor;
296+
297+
@override
298+
void paint(Canvas canvas, Size size) {
299+
const trackHeight = 12.0;
300+
final rect = Offset(0, (size.height - trackHeight) / 2) &
301+
Size(size.width, trackHeight);
302+
final radius = Radius.circular(trackHeight / 2);
303+
final track = RRect.fromRectAndRadius(rect, radius);
304+
305+
canvas.drawRRect(track, Paint()..color = trackColor);
306+
307+
final fillWidth = (size.width * progress).clamp(0.0, size.width).toDouble();
308+
if (fillWidth > 0) {
309+
final fillRect =
310+
Rect.fromLTWH(rect.left, rect.top, fillWidth, rect.height);
311+
final fill = RRect.fromRectAndRadius(fillRect, radius);
312+
canvas.drawRRect(
313+
fill,
314+
Paint()..color = accent,
315+
);
316+
}
317+
318+
final tickPaint = Paint()
319+
..color = tickColor
320+
..strokeWidth = 1
321+
..strokeCap = StrokeCap.round;
322+
for (var i = 1; i < 6; i++) {
323+
final x = rect.left + rect.width * i / 6;
324+
canvas.drawLine(
325+
Offset(x, rect.top + 3),
326+
Offset(x, rect.bottom - 3),
327+
tickPaint,
328+
);
329+
}
330+
}
331+
332+
@override
333+
bool shouldRepaint(covariant _AdjustmentTrackPainter oldDelegate) {
334+
return oldDelegate.progress != progress ||
335+
oldDelegate.accent != accent ||
336+
oldDelegate.trackColor != trackColor ||
337+
oldDelegate.tickColor != tickColor;
338+
}
339+
}

0 commit comments

Comments
 (0)