优化正在播放的自滑动刹车动画动作中

This commit is contained in:
2026-08-28 22:27:44 +08:00
parent fa396ce681
commit 2e55f94df2
+69 -95
View File
@@ -14,12 +14,11 @@ class _PlaylistPageState extends State<PlaylistPage> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final GlobalKey _currentTagKey = GlobalKey(); final GlobalKey _currentTagKey = GlobalKey();
bool _hasScrolledToCurrent = false; // ⭐ 定位事务 ID(防止旧任务污染)
int _retryCount = 0; int _scrollGeneration = 0;
static const int _maxRetries = 3;
// ⭐ 统一滚动时长 1.5 秒 // ⭐ 是否已完成定位
static const Duration _scrollDuration = Duration(milliseconds: 1500); bool _hasScrolledToCurrent = false;
@override @override
void initState() { void initState() {
@@ -35,18 +34,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
super.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<void> _scrollToCurrentSong() async { Future<void> _scrollToCurrentSong() async {
if (_hasScrolledToCurrent) return; if (_hasScrolledToCurrent) return;
@@ -58,96 +46,65 @@ class _PlaylistPageState extends State<PlaylistPage> {
return; return;
} }
// 生成新的事务 ID,使旧任务失效
final generation = ++_scrollGeneration;
// 等待页面稳定
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
if (!mounted) return; if (!mounted || generation != _scrollGeneration) return;
final totalCount = queue.length;
final avgHeight = _calculateAverageItemHeight(totalCount);
final maxExtent = _scrollController.position.maxScrollExtent;
// 获取目标 item 的当前布局状态
var tagContext = _currentTagKey.currentContext; 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 viewportHeight = _scrollController.position.viewportDimension;
final step = viewportHeight * 0.5; final roughOffset =
final direction = (currentIndex > totalCount / 2) ? -1 : 1; (currentIndex * avgHeight) - (viewportHeight / 2) + (avgHeight / 2);
final newOffset = final clamped = roughOffset.clamp(0.0, maxExtent);
(currentOffset + direction * step).clamp(0.0, maxExtent);
if ((newOffset - currentOffset).abs() < 50) { // ⭐ 瞬时跳转(用户不可见)
final bigStep = viewportHeight * 0.8 * direction; _scrollController.jumpTo(clamped);
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;
// 等待一帧,让目标 item 被构建
await WidgetsBinding.instance.endOfFrame; await WidgetsBinding.instance.endOfFrame;
if (!mounted) return; if (!mounted || generation != _scrollGeneration) return;
tagContext = _currentTagKey.currentContext; tagContext = _currentTagKey.currentContext;
if (tagContext != null) {
await _calibrateWithTag(tagContext);
_hasScrolledToCurrent = true;
return;
}
currentOffset = _scrollController.offset; // 如果还是 null,说明估算偏差太大,用迭代方式推进
if ((currentOffset - lastOffset).abs() < 10) break; if (tagContext == null) {
lastOffset = currentOffset; // 最多尝试 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 秒平滑滚动 // ⭐ 获取 Tag 的真实位置
Future<void> _calibrateWithTag(BuildContext tagContext) async { final renderBox = tagContext!.findRenderObject() as RenderBox?;
final renderBox = tagContext.findRenderObject() as RenderBox?; if (renderBox == null) {
if (renderBox == null) return; _hasScrolledToCurrent = true;
return;
}
final tagPosition = renderBox.localToGlobal(Offset.zero); final tagPosition = renderBox.localToGlobal(Offset.zero);
final tagSize = renderBox.size; final tagSize = renderBox.size;
@@ -161,13 +118,30 @@ class _PlaylistPageState extends State<PlaylistPage> {
final targetOffset = (currentOffset + delta) final targetOffset = (currentOffset + delta)
.clamp(0.0, _scrollController.position.maxScrollExtent); .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( await _scrollController.animateTo(
targetOffset, targetOffset,
duration: _scrollDuration, duration: duration,
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
); );
if (mounted && generation == _scrollGeneration) {
_hasScrolledToCurrent = true;
}
} }
@override @override