// lib/pages/playlist_page.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/audio_service.dart'; class PlaylistPage extends StatefulWidget { const PlaylistPage({super.key}); @override State createState() => _PlaylistPageState(); } class _PlaylistPageState extends State { final ScrollController _scrollController = ScrollController(); final GlobalKey _currentTagKey = GlobalKey(); bool _hasScrolledToCurrent = false; int _retryCount = 0; static const int _maxRetries = 3; // ⭐ 统一滚动时长 1.5 秒 static const Duration _scrollDuration = Duration(milliseconds: 1500); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToCurrentSong(); }); } @override void dispose() { _scrollController.dispose(); super.dispose(); } double _calculateAverageItemHeight(int itemCount) { final maxExtent = _scrollController.position.maxScrollExtent; if (maxExtent <= 0 || itemCount <= 0) return 72.0; return maxExtent / itemCount; } double _calculateTargetOffset( int currentIndex, int totalCount, double avgHeight) { final viewportHeight = _scrollController.position.viewportDimension; return (currentIndex * avgHeight) - (viewportHeight / 2) + (avgHeight / 2); } Future _scrollToCurrentSong() async { if (_hasScrolledToCurrent) return; final service = context.read(); final queue = service.queue; final currentIndex = service.currentIndex; if (queue.isEmpty || currentIndex < 0 || currentIndex >= queue.length) { return; } await Future.delayed(const Duration(milliseconds: 300)); if (!mounted) return; final totalCount = queue.length; final avgHeight = _calculateAverageItemHeight(totalCount); final maxExtent = _scrollController.position.maxScrollExtent; var tagContext = _currentTagKey.currentContext; if (tagContext != null) { await _calibrateWithTag(tagContext); _hasScrolledToCurrent = true; return; } // ⭐ 粗定位:1.5 秒平滑滚动 final targetOffset = _calculateTargetOffset(currentIndex, totalCount, avgHeight); final clampedOffset = targetOffset.clamp(0.0, maxExtent); await _scrollController.animateTo( clampedOffset, duration: _scrollDuration, curve: Curves.easeOutCubic, ); if (!mounted) return; await WidgetsBinding.instance.endOfFrame; if (!mounted) return; tagContext = _currentTagKey.currentContext; if (tagContext != null) { await _calibrateWithTag(tagContext); _hasScrolledToCurrent = true; return; } // ⭐ 迭代修正:每次 1.5 秒平滑滚动 _retryCount = 0; var currentOffset = _scrollController.offset; var lastOffset = currentOffset; while (_retryCount < _maxRetries) { _retryCount++; final viewportHeight = _scrollController.position.viewportDimension; final step = viewportHeight * 0.5; final direction = (currentIndex > totalCount / 2) ? -1 : 1; final newOffset = (currentOffset + direction * step).clamp(0.0, maxExtent); if ((newOffset - currentOffset).abs() < 50) { final bigStep = viewportHeight * 0.8 * direction; final forcedOffset = (currentOffset + bigStep).clamp(0.0, maxExtent); await _scrollController.animateTo( forcedOffset, duration: _scrollDuration, curve: Curves.easeOutCubic, ); } else { await _scrollController.animateTo( newOffset, duration: _scrollDuration, curve: Curves.easeOutCubic, ); } if (!mounted) return; await WidgetsBinding.instance.endOfFrame; if (!mounted) return; tagContext = _currentTagKey.currentContext; if (tagContext != null) { await _calibrateWithTag(tagContext); _hasScrolledToCurrent = true; return; } currentOffset = _scrollController.offset; if ((currentOffset - lastOffset).abs() < 10) break; lastOffset = currentOffset; } _hasScrolledToCurrent = true; } /// 精确定位:1.5 秒平滑滚动 Future _calibrateWithTag(BuildContext tagContext) async { final renderBox = tagContext.findRenderObject() as RenderBox?; if (renderBox == null) return; final tagPosition = renderBox.localToGlobal(Offset.zero); final tagSize = renderBox.size; final tagCenter = tagPosition.dy + tagSize.height / 2; final screenHeight = MediaQuery.of(context).size.height; final screenCenter = screenHeight / 2; final delta = tagCenter - screenCenter; final currentOffset = _scrollController.offset; final targetOffset = (currentOffset + delta) .clamp(0.0, _scrollController.position.maxScrollExtent); if ((targetOffset - currentOffset).abs() < 2) return; await _scrollController.animateTo( targetOffset, duration: _scrollDuration, curve: Curves.easeOutCubic, ); } @override Widget build(BuildContext context) { final service = context.watch(); final queue = service.queue; final currentIndex = service.currentIndex; return Scaffold( backgroundColor: const Color(0xFF0E1211), appBar: AppBar( title: const Text( '播放列表', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w500, ), ), backgroundColor: Colors.transparent, elevation: 0, foregroundColor: Colors.white, leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new), onPressed: () => Navigator.pop(context), ), actions: [ IconButton( icon: Icon(service.playModeIcon, color: Colors.white54), onPressed: service.togglePlayMode, ), IconButton( icon: const Icon(Icons.clear_all, color: Colors.white54), onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF1A1F1E), title: const Text('清空播放列表', style: TextStyle(color: Colors.white)), content: const Text('确定要清空当前播放列表吗?', style: TextStyle(color: Colors.grey)), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消')), TextButton( onPressed: () { service.clearQueue(); Navigator.pop(context); Navigator.pop(context); }, child: const Text('清空', style: TextStyle(color: Colors.red)), ), ], ), ); }, ), ], ), body: queue.isEmpty ? const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.playlist_play, size: 48, color: Colors.grey), SizedBox(height: 16), Text('播放列表为空', style: TextStyle(color: Colors.grey)), ], ), ) : ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: queue.length, itemBuilder: (context, index) { final song = queue[index]; final isCurrent = index == currentIndex; return ListTile( contentPadding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), leading: Icon( isCurrent ? Icons.play_arrow : Icons.music_note, color: isCurrent ? const Color(0xFFB8D4D0) : Colors.grey[600], size: 24, ), title: Text( song.title, style: TextStyle( fontSize: 16, fontWeight: isCurrent ? FontWeight.w600 : FontWeight.w400, color: isCurrent ? Colors.white : Colors.grey[300], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text( song.artist, style: TextStyle( fontSize: 13, color: isCurrent ? Colors.grey[400] : Colors.grey[600], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), trailing: isCurrent ? Container( key: _currentTagKey, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: const Color(0xFFB8D4D0).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: const Text( '正在播放', style: TextStyle( fontSize: 10, color: Color(0xFFB8D4D0), ), ), ) : null, onTap: () { service.setQueue(queue, startIndex: index); Navigator.pop(context); }, ); }, ), ); } }