11import 'dart:async' ;
2- import 'package:get/utils.dart' ;
32import 'package:flutter/material.dart' ;
3+ import 'package:pure_live/plugins/locale_helper.dart' ;
44import 'package:pure_live/modules/live_play/widgets/video_player/video_controller.dart' ;
55
66class 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