From 2e55f94df2c7cadc8cc570ca398b6c49ee762402 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Fri, 28 Aug 2026 22:27:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=AD=A3=E5=9C=A8=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E7=9A=84=E8=87=AA=E6=BB=91=E5=8A=A8=E5=88=B9=E8=BD=A6?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E5=8A=A8=E4=BD=9C=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/playlist_page.dart | 164 +++++++++++++++-------------------- 1 file changed, 69 insertions(+), 95 deletions(-) diff --git a/lib/pages/playlist_page.dart b/lib/pages/playlist_page.dart index b2d60a1..33943d2 100644 --- a/lib/pages/playlist_page.dart +++ b/lib/pages/playlist_page.dart @@ -14,12 +14,11 @@ class _PlaylistPageState extends State { final ScrollController _scrollController = ScrollController(); final GlobalKey _currentTagKey = GlobalKey(); - bool _hasScrolledToCurrent = false; - int _retryCount = 0; - static const int _maxRetries = 3; + // ⭐ 定位事务 ID(防止旧任务污染) + int _scrollGeneration = 0; - // ⭐ 统一滚动时长 1.5 秒 - static const Duration _scrollDuration = Duration(milliseconds: 1500); + // ⭐ 是否已完成定位 + bool _hasScrolledToCurrent = false; @override void initState() { @@ -35,18 +34,7 @@ class _PlaylistPageState extends State { 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; @@ -58,96 +46,65 @@ class _PlaylistPageState extends State { return; } + // 生成新的事务 ID,使旧任务失效 + final generation = ++_scrollGeneration; + + // 等待页面稳定 await Future.delayed(const Duration(milliseconds: 300)); - if (!mounted) return; - - final totalCount = queue.length; - final avgHeight = _calculateAverageItemHeight(totalCount); - final maxExtent = _scrollController.position.maxScrollExtent; + if (!mounted || generation != _scrollGeneration) return; + // 获取目标 item 的当前布局状态 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++; + // ⭐ 如果 Tag 还没被构建,用不可见的 jumpTo 触发布局 + if (tagContext == null) { + // 用 maxExtent 计算平均高度,做一次瞬时定位 + final maxExtent = _scrollController.position.maxScrollExtent; + final avgHeight = maxExtent / queue.length; 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); + final roughOffset = + (currentIndex * avgHeight) - (viewportHeight / 2) + (avgHeight / 2); + final clamped = roughOffset.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; + // ⭐ 瞬时跳转(用户不可见) + _scrollController.jumpTo(clamped); + // 等待一帧,让目标 item 被构建 await WidgetsBinding.instance.endOfFrame; - if (!mounted) return; + if (!mounted || generation != _scrollGeneration) return; tagContext = _currentTagKey.currentContext; - if (tagContext != null) { - await _calibrateWithTag(tagContext); - _hasScrolledToCurrent = true; - return; - } - currentOffset = _scrollController.offset; - if ((currentOffset - lastOffset).abs() < 10) break; - lastOffset = currentOffset; + // 如果还是 null,说明估算偏差太大,用迭代方式推进 + if (tagContext == null) { + // 最多尝试 2 次,每次推进半屏 + for (int i = 0; i < 2; i++) { + final viewportHeight2 = _scrollController.position.viewportDimension; + final direction = (currentIndex > queue.length / 2) ? -1 : 1; + final step = viewportHeight2 * 0.7 * direction; + final newOffset = (_scrollController.offset + step) + .clamp(0.0, _scrollController.position.maxScrollExtent); + _scrollController.jumpTo(newOffset); + await WidgetsBinding.instance.endOfFrame; + if (!mounted || generation != _scrollGeneration) return; + tagContext = _currentTagKey.currentContext; + if (tagContext != null) break; + } + if (tagContext == null) { + _hasScrolledToCurrent = true; + return; + } + } } - _hasScrolledToCurrent = true; - } + if (!mounted || generation != _scrollGeneration) return; - /// 精确定位:1.5 秒平滑滚动 - Future _calibrateWithTag(BuildContext tagContext) async { - final renderBox = tagContext.findRenderObject() as RenderBox?; - if (renderBox == null) return; + // ⭐ 获取 Tag 的真实位置 + final renderBox = tagContext!.findRenderObject() as RenderBox?; + if (renderBox == null) { + _hasScrolledToCurrent = true; + return; + } final tagPosition = renderBox.localToGlobal(Offset.zero); final tagSize = renderBox.size; @@ -161,13 +118,30 @@ class _PlaylistPageState extends State { final targetOffset = (currentOffset + delta) .clamp(0.0, _scrollController.position.maxScrollExtent); - if ((targetOffset - currentOffset).abs() < 2) return; + // 如果偏差太小,直接标记完成 + if ((targetOffset - currentOffset).abs() < 5) { + _hasScrolledToCurrent = true; + return; + } + // ⭐ 根据距离动态计算时长 + final distance = (targetOffset - currentOffset).abs(); + final durationMs = (250 + distance * 0.3).clamp(250, 700).round(); + final duration = Duration(milliseconds: durationMs); + + debugPrint( + '🎯 [定位] 距离: ${distance.toStringAsFixed(0)}px, 时长: ${durationMs}ms'); + + // ⭐ 唯一的一次可见动画 await _scrollController.animateTo( targetOffset, - duration: _scrollDuration, + duration: duration, curve: Curves.easeOutCubic, ); + + if (mounted && generation == _scrollGeneration) { + _hasScrolledToCurrent = true; + } } @override