Skip to content

Commit c42e794

Browse files
committed
修复静音
1 parent c0cd6b3 commit c42e794

3 files changed

Lines changed: 112 additions & 103 deletions

File tree

assets/translations/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,5 +557,7 @@
557557
"stream_no_available_quality": "No available quality",
558558
"stream_all_cdn_failed": "All CDN lines failed",
559559
"detected_room_id_open": "Room ID detected, open live stream?",
560-
"no_room_id_open_url": "No Room ID detected, open webpage instead?"
560+
"no_room_id_open_url": "No Room ID detected, open webpage instead?",
561+
"cancel_mute": "Unmute",
562+
"mute": "Mute"
561563
}

assets/translations/zh.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,5 +557,7 @@
557557
"stream_no_available_quality": "无可用清晰度",
558558
"stream_all_cdn_failed": "所有清晰度线路失败",
559559
"detected_room_id_open": "检测到房间号,是否打开直播间?",
560-
"no_room_id_open_url": "未检测到房间号,是否使用网页打开直播间?"
560+
"no_room_id_open_url": "未检测到房间号,是否使用网页打开直播间?",
561+
"cancel_mute": "取消静音",
562+
"mute": "静音"
561563
}

lib/modules/live_play/widgets/video_player/volume_control.dart

Lines changed: 106 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'dart:async';
2-
import 'package:get/utils.dart';
32
import 'package:flutter/material.dart';
3+
import 'package:pure_live/plugins/locale_helper.dart';
44
import 'package:pure_live/modules/live_play/widgets/video_player/video_controller.dart';
55

66
class OverlayVolumeControl extends StatefulWidget {
@@ -15,13 +15,18 @@ class _OverlayVolumeControlState extends State<OverlayVolumeControl> {
1515
double _volume = 0.5;
1616
double _lastVolume = 0.5;
1717
OverlayEntry? _overlayEntry;
18-
bool _isVolumeBarVisible = false;
1918

20-
// 鼠标追踪标志位,防止 Hover 闪烁
19+
// 用于连接图标和弹出面板的轴心
20+
final LayerLink _layerLink = LayerLink();
21+
22+
// 鼠标追踪标志位
2123
bool _isMouseInIcon = false;
2224
bool _isMouseInBar = false;
25+
Timer? _hideTimer;
26+
27+
static const double _barHeight = 150.0;
28+
static const double _barWidth = 44.0;
2329

24-
static const double _barHeight = 160; // 稍微增加高度以容纳填充
2530
VideoController get controller => widget.controller;
2631

2732
@override
@@ -32,6 +37,7 @@ class _OverlayVolumeControlState extends State<OverlayVolumeControl> {
3237

3338
@override
3439
void dispose() {
40+
_hideTimer?.cancel();
3541
_removeOverlay();
3642
super.dispose();
3743
}
@@ -45,7 +51,6 @@ class _OverlayVolumeControlState extends State<OverlayVolumeControl> {
4551
});
4652
}
4753

48-
// 静音/还原逻辑
4954
void _handleToggleMute() {
5055
setState(() {
5156
if (_volume > 0) {
@@ -61,139 +66,139 @@ class _OverlayVolumeControlState extends State<OverlayVolumeControl> {
6166

6267
// 显示音量条
6368
void _showVolumeBar() {
64-
if (_isVolumeBarVisible || !mounted) return;
65-
66-
final renderBox = context.findRenderObject() as RenderBox;
67-
final position = renderBox.localToGlobal(Offset.zero);
69+
if (_overlayEntry != null || !mounted) return;
6870

6971
_overlayEntry = OverlayEntry(
7072
builder: (context) => Positioned(
71-
// 居中对齐图标
72-
left: position.dx + (renderBox.size.width - 40) / 2,
73-
// 在图标上方显示,留出小空隙
74-
top: position.dy - _barHeight - 20,
75-
width: 40,
76-
height: _barHeight,
77-
child: MouseRegion(
78-
onEnter: (_) => _isMouseInBar = true,
79-
onExit: (_) {
80-
_isMouseInBar = false;
81-
_checkAndHide();
82-
},
83-
child: _buildVolumeBarUI(),
73+
width: _barWidth,
74+
height: _barHeight + 20, // 增加额外高度作为无缝缓冲区
75+
child: CompositedTransformFollower(
76+
link: _layerLink,
77+
showWhenUnlinked: false,
78+
// 将面板的底部中心,对齐到图标的顶部中心
79+
followerAnchor: Alignment.bottomCenter,
80+
targetAnchor: Alignment.topCenter,
81+
offset: const Offset(0, 5), // 微调向下偏移,覆盖两组件之间的空隙
82+
child: MouseRegion(
83+
onEnter: (_) => _isMouseInBar = true,
84+
onExit: (_) {
85+
_isMouseInBar = false;
86+
_startHideTimer();
87+
},
88+
child: _buildVolumeBarUI(),
89+
),
8490
),
8591
),
8692
);
8793

8894
Overlay.of(context).insert(_overlayEntry!);
89-
setState(() => _isVolumeBarVisible = true);
9095
}
9196

92-
// 检查并隐藏(核心修复:延时判断)
93-
void _checkAndHide() {
94-
Future.delayed(const Duration(milliseconds: 100), () {
95-
if (!_isMouseInIcon && !_isMouseInBar && _isVolumeBarVisible) {
97+
// 延时关闭定时器(防闪烁防抖)
98+
void _startHideTimer() {
99+
_hideTimer?.cancel();
100+
_hideTimer = Timer(const Duration(milliseconds: 150), () {
101+
if (!_isMouseInIcon && !_isMouseInBar) {
96102
_removeOverlay();
97103
}
98104
});
99105
}
100106

101107
void _removeOverlay() {
102-
try {
103-
_overlayEntry?.remove();
104-
_overlayEntry = null;
105-
if (mounted && _isVolumeBarVisible) {
106-
setState(() => _isVolumeBarVisible = false);
107-
} else {
108-
_isVolumeBarVisible = false;
109-
}
110-
} catch (e) {
111-
_isVolumeBarVisible = false;
112-
}
113-
}
114-
115-
void _handleVolumeDrag(DragUpdateDetails details) {
116-
// 允许更细腻的滑动控制
117-
final deltaRatio = -details.delta.dy / (_barHeight - 40);
118-
final newVolume = (_volume + deltaRatio).clamp(0.0, 1.0);
119-
120-
if (newVolume != _volume) {
121-
setState(() {
122-
_volume = newVolume;
123-
if (_volume > 0) _lastVolume = _volume;
124-
});
125-
_overlayEntry?.markNeedsBuild();
126-
controller.setVolume(_volume);
127-
}
108+
_overlayEntry?.remove();
109+
_overlayEntry = null;
128110
}
129111

130112
Widget _buildVolumeBarUI() {
131113
return Material(
132114
color: Colors.transparent,
133-
child: Container(
134-
padding: const EdgeInsets.only(bottom: 10), // 底部留白,方便鼠标滑向图标
115+
child: Padding(
116+
padding: const EdgeInsets.only(bottom: 12), // 底部的 Padding 可以充当鼠标滑过的桥梁,防止断连
135117
child: Container(
136118
decoration: BoxDecoration(
137-
color: Colors.black.withValues(alpha: 0.85),
138-
borderRadius: BorderRadius.circular(20),
119+
color: Colors.black.withAlpha(220),
120+
borderRadius: BorderRadius.circular(22),
139121
border: Border.all(color: Colors.white10),
140122
),
141-
child: GestureDetector(
142-
onVerticalDragUpdate: _handleVolumeDrag,
143-
child: Stack(
144-
alignment: Alignment.bottomCenter,
145-
children: [
146-
// 背景背景槽
147-
Container(
148-
width: 4,
149-
margin: const EdgeInsets.symmetric(vertical: 20),
150-
decoration: BoxDecoration(color: Colors.white12, borderRadius: BorderRadius.circular(2)),
151-
),
152-
// 音量填充
153-
Positioned(
154-
bottom: 20,
155-
child: Container(
156-
width: 4,
157-
height: _volume * (_barHeight - 50),
158-
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(2)),
159-
),
160-
),
161-
// 滑块
162-
Positioned(
163-
bottom: 20 + (_volume * (_barHeight - 50)) - 6,
164-
child: Container(
165-
width: 12,
166-
height: 12,
167-
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
168-
),
123+
child: LayoutBuilder(
124+
builder: (context, constraints) {
125+
// 实际可滑动区域的高度(扣除上下 Padding)
126+
final double trackHeight = constraints.maxHeight - 40;
127+
return GestureDetector(
128+
onVerticalDragUpdate: (details) => _handleVolumeDrag(details, trackHeight),
129+
child: Stack(
130+
alignment: Alignment.bottomCenter,
131+
children: [
132+
// 背景音量槽
133+
Container(
134+
width: 4,
135+
margin: const EdgeInsets.symmetric(vertical: 20),
136+
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(2)),
137+
),
138+
// 进度条填充
139+
Positioned(
140+
bottom: 20,
141+
child: Container(
142+
width: 4,
143+
height: _volume * trackHeight,
144+
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(2)),
145+
),
146+
),
147+
// 顶端滑块圆点
148+
Positioned(
149+
bottom: 20 + (_volume * trackHeight) - 6,
150+
child: Container(
151+
width: 12,
152+
height: 12,
153+
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
154+
),
155+
),
156+
],
169157
),
170-
],
171-
),
158+
);
159+
},
172160
),
173161
),
174162
),
175163
);
176164
}
177165

166+
void _handleVolumeDrag(DragUpdateDetails details, double trackHeight) {
167+
if (trackHeight <= 0) return;
168+
// 根据实际高度精准计算灵敏度
169+
final deltaRatio = -details.delta.dy / trackHeight;
170+
final newVolume = (_volume + deltaRatio).clamp(0.0, 1.0);
171+
if (newVolume != _volume) {
172+
setState(() {
173+
_volume = newVolume;
174+
if (_volume > 0) _lastVolume = _volume;
175+
});
176+
_overlayEntry?.markNeedsBuild();
177+
controller.setVolume(_volume);
178+
}
179+
}
180+
178181
@override
179182
Widget build(BuildContext context) {
180183
IconData icon = _volume == 0 ? Icons.volume_off : (_volume < 0.5 ? Icons.volume_down : Icons.volume_up);
181184

182-
return MouseRegion(
183-
onEnter: (_) {
184-
_isMouseInIcon = true;
185-
Duration(milliseconds: 300).delay(() {
185+
// 将原生的图标组件包裹在联动 Target 中
186+
return CompositedTransformTarget(
187+
link: _layerLink,
188+
child: MouseRegion(
189+
onEnter: (_) {
190+
_isMouseInIcon = true;
186191
_showVolumeBar();
187-
});
188-
},
189-
onExit: (_) {
190-
_isMouseInIcon = false;
191-
_checkAndHide();
192-
},
193-
child: IconButton(
194-
onPressed: _handleToggleMute,
195-
icon: Icon(icon, color: Colors.white, size: 24),
196-
tooltip: _volume == 0 ? "取消静音" : "静音",
192+
},
193+
onExit: (_) {
194+
_isMouseInIcon = false;
195+
_startHideTimer();
196+
},
197+
child: IconButton(
198+
onPressed: _handleToggleMute,
199+
icon: Icon(icon, color: Colors.white, size: 24),
200+
tooltip: _volume == 0 ? i18n('cancel_mute') : i18n('mute'),
201+
),
197202
),
198203
);
199204
}

0 commit comments

Comments
 (0)